Spaces:
Running
Running
| <script setup lang="ts"> | |
| import { ref } from 'vue' | |
| import { marked } from 'marked' | |
| import type { Citation } from '../types' | |
| import type { Lang } from '../i18n' | |
| import { t } from '../i18n' | |
| import { analyzeImage } from '../api' | |
| import Wc3Button from '../wc3/base/Button.vue' | |
| import CitationPanel from './CitationPanel.vue' | |
| const props = defineProps<{ | |
| model: string | |
| showCitations: boolean | |
| lang: Lang | |
| }>() | |
| const selectedFile = ref<File | null>(null) | |
| const imagePreview = ref('') | |
| const question = ref('') | |
| const analysis = ref('') | |
| const citations = ref<Citation[]>([]) | |
| const isLoading = ref(false) | |
| const error = ref('') | |
| const fileInput = ref<HTMLInputElement | null>(null) | |
| function renderMd(text: string): string { | |
| return marked.parse(text) as string | |
| } | |
| function handleFileSelect(e: Event) { | |
| const input = e.target as HTMLInputElement | |
| if (input.files?.[0]) { | |
| selectedFile.value = input.files[0] | |
| imagePreview.value = URL.createObjectURL(input.files[0]) | |
| analysis.value = '' | |
| citations.value = [] | |
| error.value = '' | |
| } | |
| } | |
| function handleDrop(e: DragEvent) { | |
| e.preventDefault() | |
| if (e.dataTransfer?.files[0]) { | |
| selectedFile.value = e.dataTransfer.files[0] | |
| imagePreview.value = URL.createObjectURL(e.dataTransfer.files[0]) | |
| analysis.value = '' | |
| citations.value = [] | |
| error.value = '' | |
| } | |
| } | |
| async function handleAnalyze() { | |
| if (!selectedFile.value || isLoading.value) return | |
| isLoading.value = true | |
| error.value = '' | |
| analysis.value = '' | |
| citations.value = [] | |
| try { | |
| const res = await analyzeImage(selectedFile.value, question.value, props.model) | |
| analysis.value = res.analysis | |
| citations.value = res.citations | |
| } catch (e: any) { | |
| error.value = e.message || t('image.error', props.lang) | |
| } finally { | |
| isLoading.value = false | |
| } | |
| } | |
| </script> | |
| <template> | |
| <div class="image-tab"> | |
| <div | |
| class="upload-zone" | |
| @click="fileInput?.click()" | |
| @drop="handleDrop" | |
| @dragover.prevent | |
| > | |
| <input ref="fileInput" type="file" accept="image/*" @change="handleFileSelect" /> | |
| <div v-if="!imagePreview"> | |
| <div class="icon">📄</div> | |
| <p>{{ t('image.dropHint', lang) }}</p> | |
| <p style="font-size: 0.8rem; color: var(--wc3-text-muted);"> | |
| {{ t('image.uploadHint', lang) }} | |
| </p> | |
| </div> | |
| <img v-else :src="imagePreview" class="image-preview" alt="Preview" /> | |
| </div> | |
| <div v-if="selectedFile" style="margin-bottom: 1rem;"> | |
| <input | |
| class="wc3-input" | |
| v-model="question" | |
| :placeholder="t('image.questionPlaceholder', lang)" | |
| /> | |
| </div> | |
| <div v-if="selectedFile" style="margin-bottom: 1.5rem; height: 38px; min-width: 160px; display: inline-block;"> | |
| <Wc3Button size="s" @click="handleAnalyze" :disabled="isLoading"> | |
| {{ isLoading ? t('image.analyzing', lang) : t('image.analyze', lang) }} | |
| </Wc3Button> | |
| </div> | |
| <div v-if="isLoading" class="loading"> | |
| <span>{{ t('image.analyzingStatus', lang) }}</span> | |
| <span class="dot-pulse"><span></span><span></span><span></span></span> | |
| </div> | |
| <div v-if="error" class="error-msg" style="margin-bottom: 1rem;">⚠️ {{ error }}</div> | |
| <div v-if="analysis" class="analysis-result"> | |
| <div v-html="renderMd(analysis)"></div> | |
| <CitationPanel | |
| v-if="showCitations && citations.length" | |
| :citations="citations" | |
| :lang="lang" | |
| style="margin-top: 0.75rem;" | |
| /> | |
| </div> | |
| </div> | |
| </template> | |