<script lang="ts" setup> import type { AcceptableValue } from 'reka-ui' import { toast } from 'vue-sonner' import { generateLessonPlan, getLessonPlanTemplate, regenerateLessonPlanSection, type LessonPlanTemplate, type AIGeneratedContentResponse, type GenerateLessonPlanParams, } from '~/api/aifn' import { uploadFile } from '~/api/file' import type { AIGeneratedContent } from '~/components/ai' import type { FetchError } from '~/types' const tab = ref('text') const loading = ref(false) // const input = ref('') const selectedModuleNames = ref([]) const selectedFiles = ref<File[]>([]) const onFileChange = (files: FileList) => { selectedFiles.value = Array.from(files) } const form = reactive({ query: '', urls: [] as string[], moduleName: '', }) const contentRequirements = ref<'custom' | 'template'>('custom') const data = ref<AIGeneratedContent | null>(null) const sections = [ '学情分析', '教学目标', '课程重点', '课程难点', '教学准备', '教学过程', '作业与评价', '教学反思', '思政案例', '思政元素', '教学价值与分析', ] const { data: templates } = useAsyncData(() => getLessonPlanTemplate()) const selectedTemplate = ref<LessonPlanTemplate | null>(null) const onGenerateClick = async () => { if (form.query) { let payload: GenerateLessonPlanParams = { query: form.query, } loading.value = true if (tab.value === 'text') { if (selectedFiles.value.length > 0) { for (const file of selectedFiles.value) { form.urls.push(await uploadFile(file, 'temp')) } console.log('uploaded urls', form.urls) payload = { ...payload, urls: form.urls, } } if (contentRequirements.value === 'custom') { if (!form.moduleName) { toast.error('请选择要生成的内容模块') return } payload = { ...payload, moduleName: form.moduleName, } } else if (contentRequirements.value === 'template') { if (!selectedTemplate.value) { toast.error('请选择模版') return } payload = { ...payload, useTemplate: true, templateId: `${selectedTemplate.value.id}`, } } } toast.promise(generateLessonPlan(payload), { loading: '生成中...', success: (res: AIGeneratedContentResponse) => { if (res.code !== 200) { data.value = null toast.error(res.msg || '生成失败') return } data.value = res.data return '生成成功' }, error: (err: FetchError) => { data.value = null return err.message }, finally: () => { loading.value = false }, }) } else { data.value = null } } const onSectionRegenerateClick = ( itemIndex: number, conversationId: string, ) => { if (data.value) { const item = data.value.sections[itemIndex] if (item) { loading.value = true toast.promise( regenerateLessonPlanSection({ query: `重新生成${item.title}`, conversationId, }), { loading: `重新生成${item.title}中...`, success: (res: AIGeneratedContentResponse) => { if (res.code !== 200) { toast.error(res.msg || '生成失败') return } data.value?.sections.splice(itemIndex, 1, res.data.sections[0]) return `重新生成${item.title}成功` }, error: (err: FetchError) => { return err.message }, finally: () => { loading.value = false }, }, ) } } } </script> <template> <div class="flex flex-col gap-4"> <Tabs v-model="tab" class="w-[400px]" > <TabsList> <TabsTrigger value="text"> <div class="flex items-center gap-1"> <Icon name="tabler:article" /> <span>文本生成</span> </div> </TabsTrigger> <TabsTrigger value="bot"> <div class="flex items-center gap-1"> <Icon name="tabler:robot" /> <span>课程智能体</span> </div> </TabsTrigger> </TabsList> </Tabs> <div class="flex items-start gap-4"> <div v-if="tab === 'bot'" class="flex h-full flex-col justify-center items-center gap-1 px-8 rounded-md border" > <Icon name="tabler:text-plus" class="text-3xl" /> <span class="text-xs font-medium">选择智能体</span> </div> <UniFileSelector v-if="tab === 'text'" class="h-full" placeholder="上传资料" multiple @change="onFileChange" /> <Textarea v-model="form.query" placeholder="请输入文本来生成内容" class="h-24 flex-1" /> <div class="flex flex-col items-center gap-2"> <Button size="lg" :disabled="loading" @click="onGenerateClick" > <Icon v-if="loading" name="svg-spinners:180-ring-with-bg" /> <Icon v-else name="mage:stars-c-fill" /> 生成教案 </Button> <p class="text-xs text-foreground/40">内容由 AI 生成,仅供参考</p> </div> </div> <div class="flex gap-4 items-start pb-2"> <!-- <p class="text-sm text-muted-foreground font-medium">内容要求</p> --> <div class="flex flex-col gap-4 pt-1"> <RadioGroup v-model="contentRequirements" default-value="custom" class="flex flex-row gap-4" > <div class="flex items-center space-x-2"> <RadioGroupItem id="custom" value="custom" /> <Label for="custom">自定义教案内容</Label> </div> <div class="flex items-center space-x-2"> <RadioGroupItem id="template" value="template" /> <Label for="template">模版库选择</Label> </div> </RadioGroup> <ToggleGroup v-if="contentRequirements === 'custom'" v-model="selectedModuleNames" type="multiple" variant="outline" size="sm" class="flex flex-wrap justify-start items-center gap-2 *:!px-2 *:!h-7" @update:model-value=" (value) => { form.moduleName = Array.from(value as AcceptableValue[]).join(',') } " > <ToggleGroupItem v-for="section in sections" :key="section" class="data-[state=on]:bg-primary data-[state=on]:text-primary-foreground" :value="section" > {{ section }} </ToggleGroupItem> </ToggleGroup> <div v-else class="flex gap-4 overflow-hidden overflow-x-auto snap-x snap-mandatory" > <!-- {{ templates?.data }} --> <Card v-for="template in templates?.data || []" :key="template.id" class="flex-shrink-0 p-0 overflow-hidden relative snap-start snap-always snap-x" :class="{ 'border-primary': selectedTemplate?.id === template.id, }" @click=" () => { selectedTemplate = template } " > <CardHeader class="px-4 pt-3 pb-1"> <h1 class="text-sm font-medium flex items-center gap-1"> <Icon name="fluent-color:document-text-24" class="text-lg text-muted-foreground !stroke-[5px]" /> {{ template.name }} </h1> </CardHeader> <CardContent class="p-0"> <div v-if="selectedTemplate?.id === template.id" class="absolute inset-0" > <Icon name="tabler:check" class="absolute top-2 right-2 text-primary" /> </div> <NuxtImg :src="template.imgUrl" class="h-72" /> </CardContent> </Card> </div> </div> </div> <AiGeneratedContent :data @regenerate="onSectionRegenerateClick" @regenerate-all="onGenerateClick" /> </div> </template> <style scoped></style>