fix: parseo JSON robusto, UI upload full-width, selector en header, multiple files
- session-analyzer.ts: extractJSON() con 4 estrategias de parseo + log raw - TranscriptionsView: selector proyecto movido al header, upload card full-width - TranscriptionsView: soporte multi-file con cola, banner de progreso - TranscriptionsView: download .md con revokeObjectURL diferido - TranscriptionsView: upload deshabilitado sin proyecto seleccionado - session-analyzer + project-doc exportados como servicios independientes - i18n: keys titleView, statusParsing/Analyzing/Generating, filesLoaded - i18n: fix key title duplicado (title -> titleLabel)
This commit is contained in:
@@ -249,6 +249,7 @@
|
|||||||
},
|
},
|
||||||
"transcriptions": {
|
"transcriptions": {
|
||||||
"title": "Transcriptions",
|
"title": "Transcriptions",
|
||||||
|
"titleView": "Transcriptions",
|
||||||
"subtitle": "Manage project sessions. Upload transcripts, analyze with AI, and maintain an incremental document.",
|
"subtitle": "Manage project sessions. Upload transcripts, analyze with AI, and maintain an incremental document.",
|
||||||
"configureAI": "Configure AI",
|
"configureAI": "Configure AI",
|
||||||
"aiKeyTitle": "OpenRouter API Key",
|
"aiKeyTitle": "OpenRouter API Key",
|
||||||
@@ -276,9 +277,13 @@
|
|||||||
"selected": "selected",
|
"selected": "selected",
|
||||||
"createInKappa": "Create {count} in KAPPA",
|
"createInKappa": "Create {count} in KAPPA",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"title": "Title",
|
"titleLabel": "Title",
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
"selectProjectToCreate": "Select a project above to create stories in KAPPA",
|
"selectProjectToCreate": "Select a project above to create stories in KAPPA",
|
||||||
|
"statusParsing": "Processing files...",
|
||||||
|
"statusAnalyzing": "Analyzing session with AI...",
|
||||||
|
"statusGenerating": "Generating project document...",
|
||||||
|
"filesLoaded": "files loaded",
|
||||||
"analyzeSession": "Analyze session",
|
"analyzeSession": "Analyze session",
|
||||||
"sessionError": "Error analyzing session",
|
"sessionError": "Error analyzing session",
|
||||||
"sessionSummary": "Summary",
|
"sessionSummary": "Summary",
|
||||||
@@ -299,6 +304,7 @@
|
|||||||
"updatedAt": "Updated:",
|
"updatedAt": "Updated:",
|
||||||
"docViewer": "Session document",
|
"docViewer": "Session document",
|
||||||
"selectProjectHint": "Select a project to view its sessions",
|
"selectProjectHint": "Select a project to view its sessions",
|
||||||
|
"selectProjectFirst": "Select a project to start",
|
||||||
"sessionCount": "{count} sessions | {count} session | {count} sessions"
|
"sessionCount": "{count} sessions | {count} session | {count} sessions"
|
||||||
},
|
},
|
||||||
"projectAi": {
|
"projectAi": {
|
||||||
|
|||||||
@@ -249,6 +249,7 @@
|
|||||||
},
|
},
|
||||||
"transcriptions": {
|
"transcriptions": {
|
||||||
"title": "Transcripciones",
|
"title": "Transcripciones",
|
||||||
|
"titleView": "Transcripciones",
|
||||||
"subtitle": "Gestioná las sesiones del proyecto. Subí transcripciones, analizalas con IA y mantené un documento incremental.",
|
"subtitle": "Gestioná las sesiones del proyecto. Subí transcripciones, analizalas con IA y mantené un documento incremental.",
|
||||||
"configureAI": "Configurar IA",
|
"configureAI": "Configurar IA",
|
||||||
"aiKeyTitle": "API Key de OpenRouter",
|
"aiKeyTitle": "API Key de OpenRouter",
|
||||||
@@ -276,9 +277,13 @@
|
|||||||
"selected": "seleccionadas",
|
"selected": "seleccionadas",
|
||||||
"createInKappa": "Crear {count} en KAPPA",
|
"createInKappa": "Crear {count} en KAPPA",
|
||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"title": "Título",
|
"titleLabel": "Título",
|
||||||
"priority": "Prioridad",
|
"priority": "Prioridad",
|
||||||
"selectProjectToCreate": "Seleccioná un proyecto arriba para crear las HUs en KAPPA",
|
"selectProjectToCreate": "Seleccioná un proyecto arriba para crear las HUs en KAPPA",
|
||||||
|
"statusParsing": "Procesando archivos...",
|
||||||
|
"statusAnalyzing": "Analizando sesión con IA...",
|
||||||
|
"statusGenerating": "Generando documento del proyecto...",
|
||||||
|
"filesLoaded": "archivos cargados",
|
||||||
"analyzeSession": "Analizar sesión",
|
"analyzeSession": "Analizar sesión",
|
||||||
"sessionError": "Error al analizar la sesión",
|
"sessionError": "Error al analizar la sesión",
|
||||||
"sessionSummary": "Resumen",
|
"sessionSummary": "Resumen",
|
||||||
@@ -299,6 +304,7 @@
|
|||||||
"updatedAt": "Actualizado:",
|
"updatedAt": "Actualizado:",
|
||||||
"docViewer": "Documento de sesiones",
|
"docViewer": "Documento de sesiones",
|
||||||
"selectProjectHint": "Seleccioná un proyecto para ver sus sesiones",
|
"selectProjectHint": "Seleccioná un proyecto para ver sus sesiones",
|
||||||
|
"selectProjectFirst": "Seleccioná un proyecto para empezar",
|
||||||
"sessionCount": "{count} sesiones | {count} sesión | {count} sesiones"
|
"sessionCount": "{count} sesiones | {count} sesión | {count} sesiones"
|
||||||
},
|
},
|
||||||
"projectAi": {
|
"projectAi": {
|
||||||
|
|||||||
@@ -63,12 +63,39 @@ export async function analyzeSession(
|
|||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const jsonStr = content.replace(/```json\s*/gi, '').replace(/```\s*$/g, '').trim()
|
const jsonStr = extractJSON(content)
|
||||||
const result: SessionExtraction = JSON.parse(jsonStr)
|
const result: SessionExtraction = JSON.parse(jsonStr)
|
||||||
console.log(`[Alpha] Session analysis complete — ${result.pendingTasks.length} tasks, ${result.decisions.length} decisions`)
|
console.log(`[Alpha] Session analysis complete — ${result.pendingTasks.length} tasks, ${result.decisions.length} decisions`)
|
||||||
return result
|
return result
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[Alpha] Failed to parse session analysis:', content)
|
console.error('[Alpha] Failed to parse session analysis. Raw response:', content)
|
||||||
throw new Error('No se pudo parsear el análisis de la sesión')
|
throw new Error('No se pudo parsear el análisis de la sesión')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrae el primer objeto JSON válido del texto.
|
||||||
|
* Maneja markdown code blocks, texto antes/después, etc.
|
||||||
|
*/
|
||||||
|
function extractJSON(text: string): string {
|
||||||
|
// 1. Intentar parse directo
|
||||||
|
try { JSON.parse(text); return text } catch {}
|
||||||
|
|
||||||
|
// 2. Extraer entre ```json y ```
|
||||||
|
const jsonBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/)
|
||||||
|
if (jsonBlock) {
|
||||||
|
const candidate = jsonBlock[1].trim()
|
||||||
|
try { JSON.parse(candidate); return candidate } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Extraer entre { y } (first { to last })
|
||||||
|
const firstBrace = text.indexOf('{')
|
||||||
|
const lastBrace = text.lastIndexOf('}')
|
||||||
|
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
||||||
|
const candidate = text.slice(firstBrace, lastBrace + 1)
|
||||||
|
try { JSON.parse(candidate); return candidate } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback: devolver el texto original (lanzará error en el caller)
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
import { ref, computed, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useProjectsStore } from '@/stores/projects'
|
import { useProjectsStore } from '@/stores/projects'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore, AVAILABLE_MODELS, PROVIDER_CONFIG, hasProviderApiKey, type AIProvider } from '@/stores/settings'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { analyzeSession, type SessionExtraction } from '@/services/session-analyzer'
|
import { analyzeSession, type SessionExtraction } from '@/services/session-analyzer'
|
||||||
import { generateProjectDoc, getProjectDoc } from '@/services/project-doc'
|
import { generateProjectDoc, getProjectDoc } from '@/services/project-doc'
|
||||||
|
import { parseFile } from '@/services/parse-transcription'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -39,6 +48,8 @@ import {
|
|||||||
Hash,
|
Hash,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
ChevronDown,
|
||||||
|
Check,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -93,10 +104,10 @@ watch(selectedProjectId, async (id) => {
|
|||||||
|
|
||||||
function parseSessionOffsets(md: string) {
|
function parseSessionOffsets(md: string) {
|
||||||
const offsets: { date: string; title: string; offset: number }[] = []
|
const offsets: { date: string; title: string; offset: number }[] = []
|
||||||
const re = /## 📍 Sesión \d+: (.+?) \*\*Archivo fuente:.*?\*\* /gs
|
const re = /## 📍 Sesión \d+:\s*(.+?)\n\*\*Archivo fuente:/g
|
||||||
let match
|
let match
|
||||||
while ((match = re.exec(md)) !== null) {
|
while ((match = re.exec(md)) !== null) {
|
||||||
const title = match[1]
|
const title = match[1].trim()
|
||||||
const dateMatch = title.match(/(\d{4}-\d{2}-\d{2})/)
|
const dateMatch = title.match(/(\d{4}-\d{2}-\d{2})/)
|
||||||
offsets.push({
|
offsets.push({
|
||||||
date: dateMatch?.[1] || '',
|
date: dateMatch?.[1] || '',
|
||||||
@@ -165,18 +176,23 @@ const fileQueue = ref<QueuedFile[]>([])
|
|||||||
const activeFileIndex = ref(-1)
|
const activeFileIndex = ref(-1)
|
||||||
|
|
||||||
async function handleFiles(files: FileList | File[]) {
|
async function handleFiles(files: FileList | File[]) {
|
||||||
const { parseFile } = await import('@/services/parse-transcription')
|
console.log(`[Alpha] handleFiles: ${files.length} file(s)`)
|
||||||
const newFiles: QueuedFile[] = []
|
const newFiles: QueuedFile[] = []
|
||||||
|
parsing.value = true
|
||||||
|
|
||||||
for (const file of Array.from(files)) {
|
for (const file of Array.from(files)) {
|
||||||
|
console.log(`[Alpha] Parsing: ${file.name} (${file.size} bytes)`)
|
||||||
try {
|
try {
|
||||||
const result = await parseFile(file)
|
const result = await parseFile(file)
|
||||||
|
console.log(`[Alpha] Parsed OK: ${result.fileName}, ${result.text.length} chars`)
|
||||||
newFiles.push({ fileName: result.fileName, text: result.text, parsed: true })
|
newFiles.push({ fileName: result.fileName, text: result.text, parsed: true })
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(`[Alpha] Failed to parse ${file.name}:`, e)
|
console.error(`[Alpha] Failed to parse ${file.name}:`, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
parsing.value = false
|
||||||
|
|
||||||
if (newFiles.length === 0) return
|
if (newFiles.length === 0) return
|
||||||
|
|
||||||
fileQueue.value.push(...newFiles)
|
fileQueue.value.push(...newFiles)
|
||||||
@@ -305,7 +321,18 @@ function downloadDoc() {
|
|||||||
a.href = url
|
a.href = url
|
||||||
a.download = `${slug}-sesiones.md`
|
a.download = `${slug}-sesiones.md`
|
||||||
a.click()
|
a.click()
|
||||||
URL.revokeObjectURL(url)
|
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const allProviders: AIProvider[] = ['openrouter', 'minimax', 'opencode']
|
||||||
|
function modelsForProvider(p: AIProvider) {
|
||||||
|
return AVAILABLE_MODELS.filter(m => m.provider === p)
|
||||||
|
}
|
||||||
|
function switchModel(provider: AIProvider, modelId: string) {
|
||||||
|
if (provider !== settingsStore.provider) {
|
||||||
|
settingsStore.setActiveProvider(provider)
|
||||||
|
}
|
||||||
|
settingsStore.setModel(modelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAll() {
|
function clearAll() {
|
||||||
@@ -325,47 +352,26 @@ function clearAll() {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="@container/main flex flex-1 flex-col gap-4 px-4 lg:px-6 py-4">
|
<div class="@container/main flex flex-1 flex-col gap-4 px-4 lg:px-6 py-4">
|
||||||
<!-- Header -->
|
<!-- Global processing banner -->
|
||||||
<div class="flex items-center justify-between">
|
<div
|
||||||
<div>
|
v-if="parsing || sessionLoading || docGenerating"
|
||||||
<h1 class="text-2xl font-bold tracking-tight">{{ t('transcriptions.title') }}</h1>
|
class="flex items-center gap-3 px-4 py-2.5 rounded-lg bg-primary/5 border border-primary/20 text-sm"
|
||||||
<p class="text-sm text-muted-foreground mt-1">{{ t('transcriptions.subtitle') }}</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
v-if="!settingsStore.apiKeyConfigured"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="$emit('navigate-settings')"
|
|
||||||
>
|
>
|
||||||
<Settings2 class="size-4 mr-1" />
|
<Loader2 class="size-4 animate-spin text-primary shrink-0" />
|
||||||
{{ t('transcriptions.configureAI') }}
|
<span v-if="parsing" class="text-primary font-medium">{{ t('transcriptions.statusParsing') }}</span>
|
||||||
</Button>
|
<span v-else-if="sessionLoading" class="text-primary font-medium">{{ t('transcriptions.statusAnalyzing') }}</span>
|
||||||
<Button
|
<span v-else-if="docGenerating" class="text-primary font-medium">{{ t('transcriptions.statusGenerating') }}</span>
|
||||||
v-else-if="docMarkdown"
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
@click="downloadDoc()"
|
|
||||||
>
|
|
||||||
<FileDown class="size-4 mr-1" />
|
|
||||||
{{ t('transcriptions.downloadDoc') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Project selector + Upload -->
|
<!-- Header with inline project selector -->
|
||||||
<Card id="transcriptions-top" class="overflow-hidden">
|
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||||||
<div class="flex flex-col @md:flex-row divide-y @md:divide-y-0 @md:divide-x">
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
<!-- Project selector -->
|
<h1 class="text-2xl font-bold tracking-tight shrink-0">{{ t('transcriptions.titleView') }}</h1>
|
||||||
<div class="flex-1 p-4 space-y-2">
|
<div class="min-w-[240px] md:w-[360px]">
|
||||||
<Label class="text-xs text-muted-foreground uppercase tracking-wider font-semibold flex items-center gap-1">
|
<Select v-model="selectedProjectId">
|
||||||
<FolderOpen class="size-3" />
|
<SelectTrigger id="transcriptions-project-trigger" class="h-9 text-sm">
|
||||||
{{ t('transcriptions.selectProject') }}
|
<FolderOpen class="size-4 shrink-0" />
|
||||||
</Label>
|
<SelectValue :placeholder="t('transcriptions.selectProject')" />
|
||||||
<Select
|
|
||||||
v-model="selectedProjectId"
|
|
||||||
:placeholder="t('transcriptions.projectPlaceholder')"
|
|
||||||
>
|
|
||||||
<SelectTrigger id="transcriptions-project-trigger">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem
|
<SelectItem
|
||||||
@@ -375,17 +381,95 @@ function clearAll() {
|
|||||||
>{{ p.name }}</SelectItem>
|
>{{ p.name }}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<p v-if="docSessionCount > 0" class="text-xs text-muted-foreground">
|
</div>
|
||||||
{{ t('transcriptions.sessionCount', { count: docSessionCount }) }}
|
</div>
|
||||||
</p>
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<Button
|
||||||
|
v-if="!settingsStore.apiKeyConfigured"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="$emit('navigate-settings')"
|
||||||
|
>
|
||||||
|
<Settings2 class="size-4 mr-1" />
|
||||||
|
{{ t('transcriptions.configureAI') }}
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu v-else>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button variant="outline" size="sm" class="text-xs h-7 gap-1 font-mono">
|
||||||
|
{{ settingsStore.selectedModel?.label || settingsStore.modelId }}
|
||||||
|
<ChevronDown class="size-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" class="w-64">
|
||||||
|
<DropdownMenuLabel>{{ t('projectAi.switchModel') }}</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<template v-for="p in allProviders" :key="p">
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-if="modelsForProvider(p).length > 0"
|
||||||
|
class="flex flex-col items-start py-2 cursor-default"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2 w-full">
|
||||||
|
<span class="text-xs font-medium">{{ PROVIDER_CONFIG[p].label }}</span>
|
||||||
|
<Badge
|
||||||
|
v-if="hasProviderApiKey(p)"
|
||||||
|
variant="outline"
|
||||||
|
class="text-[9px] text-green-600 border-green-300 dark:text-green-400 dark:border-green-700"
|
||||||
|
>{{ t('projectAi.keyReady') }}</Badge>
|
||||||
|
<Badge
|
||||||
|
v-else
|
||||||
|
variant="outline"
|
||||||
|
class="text-[9px] text-muted-foreground"
|
||||||
|
>{{ t('projectAi.noKey') }}</Badge>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-for="m in modelsForProvider(p)"
|
||||||
|
:key="m.id"
|
||||||
|
:disabled="!hasProviderApiKey(p)"
|
||||||
|
@click="switchModel(p, m.id)"
|
||||||
|
class="flex items-center gap-2 py-1.5 pl-6"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
v-if="settingsStore.provider === p && settingsStore.modelId === m.id"
|
||||||
|
class="size-3.5 text-primary shrink-0"
|
||||||
|
/>
|
||||||
|
<div v-else class="size-3.5 shrink-0" />
|
||||||
|
<span class="text-xs">{{ m.label }}</span>
|
||||||
|
<code class="text-[9px] text-muted-foreground ml-auto truncate max-w-[120px]">{{ m.id }}</code>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator v-if="p !== allProviders[allProviders.length - 1]" />
|
||||||
|
</template>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
class="text-xs text-muted-foreground justify-center"
|
||||||
|
@click="$emit('navigate-settings')"
|
||||||
|
>
|
||||||
|
<Settings2 class="size-3 mr-1" />
|
||||||
|
{{ t('projectAi.settings') }}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<Button
|
||||||
|
v-if="docMarkdown"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="downloadDoc()"
|
||||||
|
>
|
||||||
|
<FileDown class="size-4 mr-1" />
|
||||||
|
{{ t('transcriptions.downloadDoc') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload zone -->
|
<!-- Upload card (full width) -->
|
||||||
|
<Card id="transcriptions-upload" class="border-dashed overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="flex-1 p-4 flex flex-col items-center justify-center gap-2 cursor-pointer border-dashed min-h-[100px]"
|
class="flex flex-col items-center justify-center gap-3 p-6 min-h-[120px] transition-opacity"
|
||||||
@drop.prevent="handleFileDrop"
|
:class="selectedProjectId ? 'cursor-pointer' : 'opacity-50 pointer-events-none'"
|
||||||
@dragover.prevent
|
@drop.prevent="selectedProjectId ? handleFileDrop($event) : null"
|
||||||
@click="openFilePicker"
|
@dragover.prevent="selectedProjectId ? null : null"
|
||||||
|
@click="selectedProjectId ? openFilePicker() : null"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
@@ -395,7 +479,13 @@ function clearAll() {
|
|||||||
class="hidden"
|
class="hidden"
|
||||||
@change="handleFileSelect"
|
@change="handleFileSelect"
|
||||||
/>
|
/>
|
||||||
<template v-if="!parsedText && !parsing">
|
<template v-if="!selectedProjectId">
|
||||||
|
<FolderOpen class="size-6 text-muted-foreground" />
|
||||||
|
<p class="text-xs text-muted-foreground text-center">
|
||||||
|
{{ t('transcriptions.selectProjectFirst') }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!parsedText && !parsing">
|
||||||
<Upload class="size-6 text-muted-foreground" />
|
<Upload class="size-6 text-muted-foreground" />
|
||||||
<p class="text-xs text-muted-foreground text-center">
|
<p class="text-xs text-muted-foreground text-center">
|
||||||
{{ t('transcriptions.dropzone') }}<br>
|
{{ t('transcriptions.dropzone') }}<br>
|
||||||
@@ -408,6 +498,11 @@ function clearAll() {
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="w-full space-y-2">
|
<div class="w-full space-y-2">
|
||||||
|
<!-- Files loaded badge -->
|
||||||
|
<div class="flex items-center gap-2 text-xs text-green-600 dark:text-green-400">
|
||||||
|
<CheckCircle2 class="size-3.5" />
|
||||||
|
<span>{{ fileQueue.length }} {{ t('transcriptions.filesLoaded') }}</span>
|
||||||
|
</div>
|
||||||
<!-- Active file info -->
|
<!-- Active file info -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<FileText class="size-5 text-primary shrink-0" />
|
<FileText class="size-5 text-primary shrink-0" />
|
||||||
@@ -437,6 +532,7 @@ function clearAll() {
|
|||||||
class="text-xs h-7"
|
class="text-xs h-7"
|
||||||
:disabled="sessionLoading || !selectedProjectId"
|
:disabled="sessionLoading || !selectedProjectId"
|
||||||
@click.stop="analyzeAsSession()"
|
@click.stop="analyzeAsSession()"
|
||||||
|
id="transcriptions-analyze-btn"
|
||||||
>
|
>
|
||||||
<Loader2 v-if="sessionLoading" class="size-3 mr-1 animate-spin" />
|
<Loader2 v-if="sessionLoading" class="size-3 mr-1 animate-spin" />
|
||||||
<Sparkles v-else class="size-3 mr-1" />
|
<Sparkles v-else class="size-3 mr-1" />
|
||||||
@@ -446,7 +542,6 @@ function clearAll() {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Stats row -->
|
<!-- Stats row -->
|
||||||
|
|||||||
Reference in New Issue
Block a user