feat: Add transcript engine API and connect ConversationHistory to real data

- Add transcript-engine service that parses Claude Code JSONL transcripts
  with session listing, message extraction, token/stats analysis, and caching
- Add transcript REST routes (sessions list, latest, by session ID, section filtering)
- Rewrite ConversationHistory to fetch from /api/transcript/* instead of mock data
- Add session pills for switching between conversation sessions
- Add stats bar footer with model, duration, tokens, and tool count
- Add TranscriptSession/TranscriptMessage types, ChatInput, InputSettings,
  PromptBar updates, TranscriptCard, and useVoiceCapture composable
This commit is contained in:
2026-02-15 20:05:27 -06:00
parent 68edc01d44
commit f3ac7986ec
10 changed files with 2246 additions and 97 deletions

View File

@@ -1,8 +1,11 @@
<script setup lang="ts">
import { ref, reactive, computed, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
import type { Agent } from '../../types/agent'
import { useVoiceCapture } from '../../composables/useVoiceCapture'
import { useCanvasStore } from '../../stores/canvas'
import ChatInput from './ChatInput.vue'
import TranscriptCard from './TranscriptCard.vue'
import InputSettings from './InputSettings.vue'
import ConversationHistory from './ConversationHistory.vue'
interface ChatMessage {
@@ -35,11 +38,17 @@ const emit = defineEmits<{
submit: [text: string]
}>()
const canvasStore = useCanvasStore()
const voice = useVoiceCapture({
onNotification: (msg, type, duration) => canvasStore.showNotification(msg, type, duration)
})
const contentEl = ref<HTMLDivElement | null>(null)
const chatInputEl = ref<InstanceType<typeof ChatInput> | null>(null)
const isRecording = ref(false)
const showTranscript = ref(false)
const showHistory = ref(false)
const showSettings = ref(false)
const messages = reactive<ChatMessage[]>([])
const panelStyle = computed(() => {
@@ -60,7 +69,7 @@ const panelStyle = computed(() => {
})
const hasContent = computed(() =>
messages.length > 0 || showTranscript.value || showHistory.value
messages.length > 0 || showTranscript.value || showHistory.value || showSettings.value
)
async function scrollToBottom() {
@@ -100,6 +109,8 @@ function handleTranscriptDone(text: string) {
isRecording.value = false
showTranscript.value = false
if (!text.trim()) return
messages.push({ id: ++idCounter, role: 'user', content: text, status: 'sent' })
const agentMsg: ChatMessage = { id: ++idCounter, role: 'agent', content: '', status: 'thinking' }
@@ -108,6 +119,10 @@ function handleTranscriptDone(text: string) {
pushAgentResponse(agentMsg)
}
function toggleSettings() {
showSettings.value = !showSettings.value
}
function toggleHistory() {
showHistory.value = !showHistory.value
if (showHistory.value) scrollToBottom()
@@ -122,14 +137,22 @@ watch(() => props.visible, async (v) => {
isRecording.value = false
showTranscript.value = false
showHistory.value = false
showSettings.value = false
messages.length = 0
idCounter = 0
await voice.init()
await nextTick()
if (props.startRecording) {
handleMic()
} else {
chatInputEl.value?.focus()
}
} else {
// Cleanup when panel closes
if (voice.isRecording.value) {
voice.stopRecording()
}
voice.cleanup()
}
})
@@ -142,6 +165,7 @@ onMounted(() => {
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKeydown)
if (thinkTimer) clearTimeout(thinkTimer)
voice.cleanup()
})
</script>
@@ -187,7 +211,8 @@ onBeforeUnmount(() => {
</template>
</div>
<TranscriptCard v-if="showTranscript" @done="handleTranscriptDone" />
<TranscriptCard v-if="showTranscript" :voice="voice" @done="handleTranscriptDone" />
<InputSettings v-if="showSettings" :voice="voice" @close="showSettings = false" />
<ConversationHistory v-if="showHistory" :agent="agent" />
</div>
@@ -197,10 +222,12 @@ onBeforeUnmount(() => {
:placeholder="`Mensaje a ${agent.uiConfig?.label || agent.name}...`"
:recording="isRecording"
:history-active="showHistory"
:settings-active="showSettings"
:autofocus="visible"
@submit="handleSubmit"
@mic="handleMic"
@toggle-history="toggleHistory"
@toggle-settings="toggleSettings"
/>
</div>
</div>