Files
geo/apps/admin-web/src/components/GenerateTaskDrawer.vue
T
root 51d32fa96d fix(admin-web): invalidate workspace cache after every AI-points-consuming call
Top-nav quota chip (会员到期 / 生成文章 / AI 点数) only refreshes when its
query is invalidated. Several mutations and stream-based flows that debit
points or article quota on the server were not invalidating it, so users
had to refresh the page to see updated balances.

- TemplateWizardView: invalidate `['workspace']` inside `endAssistTask()`,
  covering analyze / title / outline assist tasks in one place.
- FreeCreateView: invalidate after `articlesApi.create` success.
- GenerateTaskDrawer: add `['workspace']` to the instant-generate
  `Promise.all` alongside the existing `articles` / `instantTasks` keys.
- KolPromptEditor: invalidate in the `finally` of both `runAiAssistStream`
  (generate) and `generateSelectionAiPreview` (optimize) so abort / error
  paths also refresh.
- ArticleEditorCanvas: bring in `useQueryClient` and invalidate in the
  selection-optimize stream `finally`.

Together with the existing invalidations in TemplateWizard.generate,
ImitationGenerate, KolGenerate, and article-regenerate, every quota-
debiting call site now feeds the cache refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:54:12 +08:00

1019 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { MinusCircleFilled, PlusOutlined } from '@ant-design/icons-vue'
import type { PromptRuleSimple, ScheduleTask } from '@geo/shared-types'
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { message } from 'ant-design-vue'
import { computed, reactive, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import CoverPickerModal from '@/components/CoverPickerModal.vue'
import PromptRuleModal from '@/components/PromptRuleModal.vue'
import {
generateApi,
imagesApi,
mediaApi,
promptRulesApi,
resolveApiURL,
schedulesApi,
tenantAccountsApi,
} from '@/lib/api'
import { coverUploadRequired, deriveCoverFileName } from '@/lib/cover-requirements'
import { formatError } from '@/lib/errors'
import {
buildPublishAccountCards,
buildPublishPlatformMap,
publishStatusShortLabel,
type PublishAccountCard,
} from '@/lib/publish-account-cards'
const props = defineProps<{
open: boolean
mode: 'instant' | 'schedule'
task?: ScheduleTask | null
}>()
const emit = defineEmits<{
'update:open': [value: boolean]
}>()
const queryClient = useQueryClient()
const { t } = useI18n()
const promptDrawerOpen = ref(false)
const coverPickerOpen = ref(false)
const form = reactive({
name: '',
promptRuleId: undefined as number | undefined,
autoPublish: false,
publishAccountIds: [] as string[],
coverEnabled: false,
coverAssetUrl: '',
coverFileName: '',
coverImageAssetId: null as number | null,
enableWebSearch: false,
generateCount: 1,
scheduleTime: '08:00:00',
})
const isSchedule = computed(() => props.mode === 'schedule')
const rulesQuery = useQuery({
queryKey: ['promptRules', 'simple'],
queryFn: () => promptRulesApi.listSimple(),
})
const schedulePublishQueryEnabled = computed(() => props.open && isSchedule.value)
const platformsQuery = useQuery({
queryKey: ['media', 'platforms', 'schedule-task-drawer'],
enabled: schedulePublishQueryEnabled,
queryFn: () => mediaApi.platforms(),
})
const accountsQuery = useQuery({
queryKey: ['tenant', 'desktop-accounts', 'schedule-task-drawer'],
enabled: schedulePublishQueryEnabled,
queryFn: () => tenantAccountsApi.list(),
})
const promptOptions = computed(() =>
(rulesQuery.data.value ?? []).map((rule: PromptRuleSimple) => ({
label: rule.name,
value: rule.id,
})),
)
const platformMap = computed(() => buildPublishPlatformMap(platformsQuery.data.value ?? []))
const schedulePublishAccounts = computed<PublishAccountCard[]>(() =>
buildPublishAccountCards(accountsQuery.data.value ?? [], platformMap.value),
)
const schedulePublishLoading = computed(
() => platformsQuery.isPending.value || accountsQuery.isPending.value,
)
const selectedSchedulePublishPlatformIds = computed(() => {
const selected = new Set(form.publishAccountIds)
return Array.from(
new Set(
schedulePublishAccounts.value
.filter((account) => selected.has(account.id))
.map((account) => account.platformId),
),
)
})
const scheduleCoverRequired = computed(() =>
coverUploadRequired(selectedSchedulePublishPlatformIds.value),
)
const effectiveScheduleCoverEnabled = computed(
() => scheduleCoverRequired.value || form.coverEnabled,
)
watch(
scheduleCoverRequired,
(required) => {
if (required) {
form.coverEnabled = true
}
},
{ immediate: true },
)
const drawerTitle = computed(() => {
if (isSchedule.value) {
return props.task?.id ? t('custom.schedule.editTitle') : t('custom.schedule.createTitle')
}
return t('custom.instant.createTitle')
})
const submitText = computed(() => {
if (isSchedule.value) {
return props.task?.id ? t('common.save') : t('custom.task.submit')
}
return t('custom.task.submit')
})
watch(
() => props.open,
(open) => {
if (open) {
hydrateForm()
}
},
)
const createScheduleMutation = useMutation({
mutationFn: () => schedulesApi.create(buildSchedulePayload()),
onSuccess: async () => {
message.success(t('custom.messages.scheduleCreated'))
emit('update:open', false)
await queryClient.invalidateQueries({ queryKey: ['schedules'] })
},
onError: (error) => message.error(formatError(error)),
})
const updateScheduleMutation = useMutation({
mutationFn: () => schedulesApi.update(props.task!.id, buildSchedulePayload()),
onSuccess: async () => {
message.success(t('custom.messages.scheduleUpdated'))
emit('update:open', false)
await queryClient.invalidateQueries({ queryKey: ['schedules'] })
},
onError: (error) => message.error(formatError(error)),
})
const instantGenerateMutation = useMutation({
mutationFn: () => generateApi.fromRule(buildInstantPayload()),
onSuccess: async () => {
message.success(t('custom.messages.generateSubmitted'))
emit('update:open', false)
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['articles'] }),
queryClient.invalidateQueries({ queryKey: ['instantTasks'] }),
queryClient.invalidateQueries({ queryKey: ['workspace'] }),
])
},
onError: (error) => message.error(formatError(error)),
})
const submitLoading = computed(
() =>
createScheduleMutation.isPending.value ||
updateScheduleMutation.isPending.value ||
instantGenerateMutation.isPending.value,
)
function hydrateForm(): void {
form.name = props.task?.name ?? ''
form.promptRuleId = props.task?.prompt_rule_id ?? undefined
form.autoPublish = isSchedule.value ? Boolean(props.task?.auto_publish) : false
form.publishAccountIds = isSchedule.value ? [...(props.task?.publish_account_ids ?? [])] : []
form.coverAssetUrl = isSchedule.value ? resolveApiURL(props.task?.cover_asset_url) : ''
form.coverFileName = deriveCoverFileName(form.coverAssetUrl)
form.coverImageAssetId = isSchedule.value ? (props.task?.cover_image_asset_id ?? null) : null
form.coverEnabled = Boolean(form.coverAssetUrl)
form.enableWebSearch = props.task?.enable_web_search ?? false
form.generateCount = props.task?.generate_count ?? 1
form.scheduleTime = parseCronToDailyTime(props.task?.cron_expr)
}
function buildSchedulePayload() {
const coverEnabled = form.autoPublish && effectiveScheduleCoverEnabled.value
const coverAssetUrl = coverEnabled ? form.coverAssetUrl.trim() : ''
return {
name: form.name.trim(),
prompt_rule_id: form.promptRuleId!,
cron_expr: buildDailyCron(form.scheduleTime),
auto_publish: form.autoPublish,
publish_account_ids: form.autoPublish ? form.publishAccountIds : [],
cover_asset_url: coverAssetUrl || null,
cover_image_asset_id: coverAssetUrl ? form.coverImageAssetId : null,
enable_web_search: form.enableWebSearch,
generate_count: form.generateCount,
}
}
function buildInstantPayload() {
return {
prompt_rule_id: form.promptRuleId!,
extra_params: {
task_name: form.name.trim(),
generation_mode: 'instant',
enable_web_search: form.enableWebSearch,
generate_count: form.generateCount,
},
}
}
function handlePromptSaved(ruleId: number): void {
form.promptRuleId = ruleId
promptDrawerOpen.value = false
void queryClient.invalidateQueries({ queryKey: ['promptRules', 'simple'] })
}
function toggleSchedulePublishAccount(account: PublishAccountCard): void {
if (!account.selectable) {
return
}
if (form.publishAccountIds.includes(account.id)) {
form.publishAccountIds = form.publishAccountIds.filter((id) => id !== account.id)
return
}
form.publishAccountIds = [...form.publishAccountIds, account.id]
}
function isSchedulePublishAccountSelected(accountId: string): boolean {
return form.publishAccountIds.includes(accountId)
}
async function uploadScheduleCover(
file: File,
): Promise<{ url: string; fileName: string; assetId?: number | null }> {
const uploaded = await imagesApi.upload(file)
return {
url: uploaded.url,
fileName: uploaded.name,
assetId: uploaded.id,
}
}
function handleScheduleCoverPicked(payload: {
url: string
fileName: string
assetId?: number | null
}): void {
form.coverAssetUrl = payload.url
form.coverFileName = payload.fileName
form.coverImageAssetId = payload.assetId ?? null
form.coverEnabled = true
}
function handleRemoveScheduleCover(): void {
form.coverAssetUrl = ''
form.coverFileName = ''
form.coverImageAssetId = null
}
function handleScheduleCoverToggle(checked: boolean): void {
if (scheduleCoverRequired.value) {
form.coverEnabled = true
return
}
form.coverEnabled = checked
}
function validateForm(): boolean {
if (!form.name.trim()) {
message.warning(t('custom.messages.missingTaskName'))
return false
}
if (!form.promptRuleId) {
message.warning(t('custom.messages.missingPromptRule'))
return false
}
if (form.generateCount < 1) {
message.warning(t('custom.messages.invalidGenerateCount'))
return false
}
if (isSchedule.value && !isValidTime(form.scheduleTime)) {
message.warning(t('custom.messages.invalidScheduleTime'))
return false
}
if (isSchedule.value && form.autoPublish) {
if (form.publishAccountIds.length === 0) {
message.warning(t('custom.messages.missingPublishAccounts'))
return false
}
if (scheduleCoverRequired.value && !form.coverAssetUrl.trim()) {
message.warning(t('custom.messages.missingScheduleCover'))
return false
}
}
return true
}
async function handleSubmit(): Promise<void> {
if (!validateForm()) {
return
}
if (isSchedule.value) {
if (props.task?.id) {
await updateScheduleMutation.mutateAsync()
} else {
await createScheduleMutation.mutateAsync()
}
return
}
await instantGenerateMutation.mutateAsync()
}
function parseCronToDailyTime(cronExpr?: string | null): string {
if (!cronExpr) {
return '08:00:00'
}
const fiveFieldMatch = cronExpr.match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+\*$/)
if (fiveFieldMatch) {
const [, minute, hour] = fiveFieldMatch
return `${padTime(hour)}:${padTime(minute)}:00`
}
const sixFieldMatch = cronExpr.match(/^(\d{1,2})\s+(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+\*$/)
if (sixFieldMatch) {
const [, second, minute, hour] = sixFieldMatch
return `${padTime(hour)}:${padTime(minute)}:${padTime(second)}`
}
return '08:00:00'
}
function buildDailyCron(timeValue: string): string {
const [hourRaw, minuteRaw] = timeValue.split(':')
const hour = Number(hourRaw || 0)
const minute = Number(minuteRaw || 0)
return `${minute} ${hour} * * *`
}
function isValidTime(value: string): boolean {
return /^\d{2}:\d{2}(:\d{2})?$/.test(value)
}
function padTime(value: string): string {
return value.padStart(2, '0')
}
</script>
<template>
<a-drawer
:open="open"
:title="drawerTitle"
placement="right"
width="1120"
:mask-closable="false"
class="generate-task-drawer"
@close="emit('update:open', false)"
>
<div class="generate-task-drawer__body">
<section class="generate-task-drawer__panel">
<div class="generate-task-drawer__section-head">
<div class="generate-task-drawer__section-marker"></div>
<h3>{{ t('custom.task.basicSection') }}</h3>
</div>
<div class="generate-task-drawer__field">
<label class="generate-task-drawer__label generate-task-drawer__label--required">
{{ t('custom.task.name') }}
</label>
<a-input
v-model:value="form.name"
:maxlength="100"
size="large"
:placeholder="t('custom.task.namePlaceholder')"
/>
</div>
<div class="generate-task-drawer__field">
<label class="generate-task-drawer__label generate-task-drawer__label--required">
{{ t('custom.task.prompt') }}
</label>
<div class="generate-task-drawer__prompt-row">
<a-select
v-model:value="form.promptRuleId"
size="large"
:placeholder="t('custom.task.promptPlaceholder')"
:options="promptOptions"
:loading="rulesQuery.isPending.value"
allow-clear
class="generate-task-drawer__prompt-select"
/>
<a-button
class="generate-task-drawer__prompt-create"
size="large"
@click="promptDrawerOpen = true"
>
<template #icon><PlusOutlined /></template>
{{ t('custom.task.createPrompt') }}
</a-button>
</div>
</div>
</section>
<section
v-if="isSchedule"
class="generate-task-drawer__panel generate-task-drawer__panel--bordered"
>
<div class="generate-task-drawer__section-head">
<div class="generate-task-drawer__section-marker"></div>
<h3>{{ t('custom.task.publishSection') }}</h3>
</div>
<div class="generate-task-drawer__field generate-task-drawer__field--inline">
<div>
<label class="generate-task-drawer__label">{{ t('custom.task.autoPublish') }}</label>
<p class="generate-task-drawer__hint generate-task-drawer__hint--compact">
{{ t('custom.task.autoPublishHint') }}
</p>
</div>
<a-switch v-model:checked="form.autoPublish" />
</div>
<template v-if="form.autoPublish">
<div class="generate-task-drawer__field">
<label class="generate-task-drawer__label generate-task-drawer__label--required">
{{ t('custom.task.publishAccounts') }}
</label>
<p class="generate-task-drawer__hint">{{ t('custom.task.publishAccountsHint') }}</p>
<a-skeleton
v-if="schedulePublishLoading"
active
:title="false"
:paragraph="{ rows: 4 }"
/>
<div
v-else-if="schedulePublishAccounts.length"
class="generate-task-drawer__account-grid"
>
<button
v-for="account in schedulePublishAccounts"
:key="account.id"
type="button"
class="generate-task-drawer__account"
:class="{
'generate-task-drawer__account--active': isSchedulePublishAccountSelected(
account.id,
),
'generate-task-drawer__account--disabled': !account.selectable,
}"
@click="toggleSchedulePublishAccount(account)"
>
<span class="generate-task-drawer__account-check">
<span v-if="isSchedulePublishAccountSelected(account.id)"></span>
</span>
<span
class="generate-task-drawer__account-logo"
:style="{ '--account-accent': account.platformAccent }"
>
<img
v-if="account.platformLogoUrl"
:src="account.platformLogoUrl"
:alt="account.platformName"
referrerpolicy="no-referrer"
/>
<span v-else>{{ account.platformShortName }}</span>
</span>
<span class="generate-task-drawer__account-copy">
<strong>{{ account.displayName }}</strong>
<small>{{ account.platformName }} · {{ account.platformUid }}</small>
</span>
<a-tooltip :title="account.statusText" placement="top">
<span
class="generate-task-drawer__account-status"
:class="`generate-task-drawer__account-status--${account.publishState}`"
>
{{ publishStatusShortLabel(account.publishState) }}
</span>
</a-tooltip>
</button>
</div>
<div v-else class="generate-task-drawer__empty">
<strong>{{ t('custom.task.publishAccountEmptyTitle') }}</strong>
<p>{{ t('custom.task.publishAccountEmptyHint') }}</p>
</div>
</div>
<div class="generate-task-drawer__field">
<div class="generate-task-drawer__cover-head">
<label
class="generate-task-drawer__label"
:class="{ 'generate-task-drawer__label--required': scheduleCoverRequired }"
>
{{ t('custom.task.cover') }}
</label>
<a-switch
:checked="effectiveScheduleCoverEnabled"
:disabled="scheduleCoverRequired"
@change="handleScheduleCoverToggle"
/>
</div>
<p class="generate-task-drawer__hint">
{{
scheduleCoverRequired
? t('custom.task.scheduleCoverRequiredHint')
: t('custom.task.scheduleCoverHint')
}}
</p>
<div v-if="effectiveScheduleCoverEnabled" class="generate-task-drawer__cover-body">
<div class="generate-task-drawer__cover-preview-wrap">
<button
type="button"
class="generate-task-drawer__cover-preview"
@click="coverPickerOpen = true"
>
<template v-if="form.coverAssetUrl">
<img :src="form.coverAssetUrl" alt="cover preview" />
</template>
<template v-else>
<span class="generate-task-drawer__cover-plus">+</span>
<span>{{ t('custom.task.coverUpload') }}</span>
</template>
</button>
<button
v-if="form.coverAssetUrl"
type="button"
class="generate-task-drawer__cover-remove"
:aria-label="t('article.editor.coverRemove')"
:title="t('article.editor.coverRemove')"
@click.stop="handleRemoveScheduleCover"
>
<MinusCircleFilled />
</button>
</div>
</div>
</div>
</template>
</section>
<section class="generate-task-drawer__panel generate-task-drawer__panel--bordered">
<div class="generate-task-drawer__section-head">
<div class="generate-task-drawer__section-marker"></div>
<h3>{{ t('custom.task.advancedSection') }}</h3>
</div>
<div v-if="isSchedule" class="generate-task-drawer__field">
<label class="generate-task-drawer__label">{{ t('custom.task.scheduleTime') }}</label>
<div class="generate-task-drawer__time-wrapper">
<span>{{ t('custom.task.scheduleEveryDay') }}</span>
<a-time-picker
v-model:value="form.scheduleTime"
value-format="HH:mm:ss"
size="large"
:allow-clear="false"
style="width: 160px"
/>
</div>
</div>
<div class="generate-task-drawer__field generate-task-drawer__field--inline">
<label class="generate-task-drawer__label">
{{ t('custom.task.enableWebSearch') }}
</label>
<a-switch v-model:checked="form.enableWebSearch" />
</div>
<div class="generate-task-drawer__field generate-task-drawer__field--narrow">
<label class="generate-task-drawer__label">{{ t('custom.task.generateCount') }}</label>
<a-input-number
v-model:value="form.generateCount"
:min="1"
:max="20"
:precision="0"
size="large"
/>
</div>
</section>
</div>
<template #footer>
<div class="generate-task-drawer__footer">
<a-button size="large" @click="emit('update:open', false)">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" size="large" :loading="submitLoading" @click="handleSubmit">
{{ submitText }}
</a-button>
</div>
</template>
<PromptRuleModal v-model:open="promptDrawerOpen" @saved="handlePromptSaved" />
<CoverPickerModal
v-model:open="coverPickerOpen"
:article-id="null"
:platform-ids="selectedSchedulePublishPlatformIds"
:current-url="form.coverAssetUrl"
:current-file-name="form.coverFileName"
:current-asset-id="form.coverImageAssetId"
:upload-handler="uploadScheduleCover"
@confirmed="handleScheduleCoverPicked"
/>
</a-drawer>
</template>
<style scoped>
.generate-task-drawer :deep(.ant-drawer-header) {
padding: 30px 28px;
border-bottom: 1px solid #edf2f7;
}
.generate-task-drawer :deep(.ant-drawer-title) {
color: #111827;
font-size: 18px;
font-weight: 800;
}
.generate-task-drawer :deep(.ant-drawer-body) {
padding: 30px 32px 32px;
background: #fff;
}
.generate-task-drawer :deep(.ant-drawer-footer) {
padding: 18px 24px;
border-top: 1px solid #edf2f7;
background: #fff;
}
.generate-task-drawer__body {
padding: 0 20px;
display: flex;
flex-direction: column;
}
.generate-task-drawer__panel {
padding: 16px 0 32px;
}
.generate-task-drawer__panel--bordered {
padding-top: 32px;
border-top: 1px solid #edf2f7;
}
.generate-task-drawer__section-head {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 20px;
border-bottom: 1px solid #fafafa;
}
.generate-task-drawer__section-marker {
width: 4px;
height: 16px;
border-radius: 2px;
background: #111827;
}
.generate-task-drawer__section-head h3 {
margin: 0;
color: #111827;
font-size: 16px;
font-weight: 800;
}
.generate-task-drawer__field {
margin-top: 24px;
}
.generate-task-drawer__field--inline {
display: flex;
align-items: center;
justify-content: space-between;
}
.generate-task-drawer__field--narrow {
max-width: 220px;
}
.generate-task-drawer__label {
display: inline-flex;
align-items: center;
margin-bottom: 12px;
color: #1f2937;
font-size: 15px;
font-weight: 700;
}
.generate-task-drawer__label--required::before {
content: '*';
margin-right: 4px;
color: #ff4d4f;
}
.generate-task-drawer__hint {
margin: -2px 0 14px;
color: #667085;
font-size: 13px;
line-height: 1.7;
}
.generate-task-drawer__hint--compact {
margin-top: 6px;
}
.generate-task-drawer__prompt-row {
display: flex;
gap: 12px;
align-items: center;
}
.generate-task-drawer__prompt-select {
flex: 0 1 320px;
}
.generate-task-drawer__prompt-create {
border-style: dashed;
border-color: #9db7ff;
color: #355dff;
}
.generate-task-drawer__account-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
}
.generate-task-drawer__account {
display: grid;
grid-template-columns: 20px 36px minmax(0, 1fr);
gap: 12px;
align-items: center;
min-height: 76px;
padding: 12px 14px;
border: 1px solid #e1e8f2;
border-radius: 8px;
background: #fff;
text-align: left;
cursor: pointer;
transition:
border-color 0.2s ease,
background 0.2s ease,
box-shadow 0.2s ease;
}
.generate-task-drawer__account:hover {
border-color: #b7c7ec;
background: #fbfdff;
}
.generate-task-drawer__account--active {
border-color: #355dff;
background: #f4f7ff;
box-shadow: 0 4px 12px rgba(53, 93, 255, 0.08);
}
.generate-task-drawer__account--disabled {
cursor: not-allowed;
opacity: 0.55;
}
.generate-task-drawer__account-check {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border: 1px solid #d0d7e5;
border-radius: 4px;
background: #fff;
}
.generate-task-drawer__account--active .generate-task-drawer__account-check {
border-color: #355dff;
background: #355dff;
}
.generate-task-drawer__account-check span {
width: 6px;
height: 6px;
border-radius: 1px;
background: #fff;
}
.generate-task-drawer__account-logo {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
border: 1px solid #e4e9f2;
background: #fff;
color: var(--account-accent);
font-size: 12px;
font-weight: 800;
overflow: hidden;
}
.generate-task-drawer__account-logo img {
width: 24px;
height: 24px;
object-fit: contain;
}
.generate-task-drawer__account-logo span {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background: color-mix(in srgb, var(--account-accent) 10%, #ffffff);
}
.generate-task-drawer__account-copy {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.generate-task-drawer__account-copy strong {
overflow: hidden;
color: #1f2937;
font-size: 14px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.generate-task-drawer__account-copy small,
.generate-task-drawer__account-status {
overflow: hidden;
color: #667085;
font-size: 12px;
line-height: 1.4;
text-overflow: ellipsis;
white-space: nowrap;
}
.generate-task-drawer__account-status {
grid-column: 3;
color: #355dff;
}
.generate-task-drawer__account-status--immediate {
color: #039855;
}
.generate-task-drawer__account-status--queued {
color: #dc6803;
}
.generate-task-drawer__account-status--unavailable {
color: #d92d20;
}
.generate-task-drawer__cover-head {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 12px;
}
.generate-task-drawer__cover-head .generate-task-drawer__label {
margin-bottom: 0;
}
.generate-task-drawer__empty {
padding: 18px 20px;
border: 1px dashed #cfd9e8;
border-radius: 8px;
background: #fbfcff;
}
.generate-task-drawer__empty strong {
display: block;
color: #1f2937;
font-size: 15px;
font-weight: 700;
}
.generate-task-drawer__empty p {
margin: 6px 0 0;
color: #667085;
font-size: 13px;
line-height: 1.7;
}
.generate-task-drawer__cover-body {
display: flex;
margin-top: 12px;
}
.generate-task-drawer__cover-preview-wrap {
position: relative;
width: 176px;
flex: 0 0 176px;
}
.generate-task-drawer__cover-preview {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
min-height: 124px;
padding: 0;
border: 1px dashed #d1d5db;
border-radius: 8px;
background: #f9fafb;
color: #6b7280;
font-size: 13px;
cursor: pointer;
overflow: hidden;
transition: all 0.2s ease;
}
.generate-task-drawer__cover-preview:hover {
border-color: #6366f1;
background: #f5f3ff;
color: #6366f1;
}
.generate-task-drawer__cover-preview img {
display: block;
width: 100%;
height: 124px;
object-fit: cover;
}
.generate-task-drawer__cover-plus {
font-size: 20px;
line-height: 1;
}
.generate-task-drawer__cover-remove {
position: absolute;
top: 8px;
right: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
border-radius: 999px;
background: rgba(17, 24, 39, 0.7);
color: #fff;
cursor: pointer;
transition: background 0.2s ease;
}
.generate-task-drawer__cover-remove:hover {
background: rgba(17, 24, 39, 0.9);
}
.generate-task-drawer__time-wrapper {
display: flex;
align-items: center;
gap: 12px;
}
.generate-task-drawer__time-wrapper span {
color: #344054;
font-size: 15px;
font-weight: 600;
}
.generate-task-drawer__footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
@media (max-width: 1024px) {
.generate-task-drawer__prompt-row {
flex-direction: column;
align-items: stretch;
}
.generate-task-drawer__prompt-select {
flex-basis: auto;
}
}
</style>