Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="flex flex-col h-full bg-slate-50/30"> | |
| <!-- Header Controls --> | |
| <div | |
| v-if="messages.length > 0" | |
| class="px-4 py-2 border-b border-slate-100 flex justify-end shrink-0" | |
| > | |
| <UButton | |
| icon="i-lucide-trash-2" | |
| color="gray" | |
| variant="ghost" | |
| size="xs" | |
| label="Clear Chat" | |
| @click="chatStore.clearHistory(project.id)" | |
| class="text-slate-400 hover:text-red-500 transition-colors" | |
| /> | |
| </div> | |
| <div | |
| class="flex-1 p-4 overflow-y-auto custom-scrollbar space-y-4" | |
| ref="chatContainer" | |
| > | |
| <!-- Welcome message --> | |
| <div | |
| v-if="messages.length === 0" | |
| class="h-full flex flex-col items-center justify-center text-center p-6 animate-fade-in-up" | |
| > | |
| <div | |
| class="w-16 h-16 rounded-2xl bg-gradient-to-br from-green-400 to-green-600 flex items-center justify-center mb-6 shadow-lg shadow-green-500/20" | |
| > | |
| <UIcon name="i-lucide-bot" class="w-8 h-8 text-white" /> | |
| </div> | |
| <h3 class="font-bold text-slate-900 mb-2">AI Tutor Ready</h3> | |
| <p class="text-sm text-slate-500 mb-8 max-w-[240px]"> | |
| Ask me any questions about the video or educational concepts. | |
| </p> | |
| <!-- Vertical Suggestion Chips --> | |
| <div | |
| v-if="suggestions.length > 0" | |
| class="flex flex-col gap-2.5 w-full max-w-[300px]" | |
| > | |
| <p | |
| class="text-[10px] uppercase tracking-[0.1em] font-bold text-slate-400 mb-1 text-left px-1" | |
| > | |
| Suggested for you | |
| </p> | |
| <button | |
| v-for="suggestion in suggestions" | |
| :key="suggestion" | |
| class="group flex items-center gap-3 px-4 py-3 rounded-xl bg-white border border-slate-100 text-left text-sm text-slate-700 hover:border-green-500 hover:bg-green-50/30 transition-all duration-200 shadow-sm hover:shadow-md" | |
| @click="useSuggestion(suggestion)" | |
| > | |
| <UIcon | |
| name="i-lucide-sparkles" | |
| class="w-4 h-4 text-green-500 opacity-40 group-hover:opacity-100 transition-opacity shrink-0" | |
| /> | |
| <span class="whitespace-normal leading-relaxed text-slate-700">{{ | |
| suggestion | |
| }}</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Messages --> | |
| <div | |
| v-for="(msg, i) in messages" | |
| :key="i" | |
| class="flex animate-fade-in-up" | |
| :class="msg.role === 'user' ? 'justify-end' : 'justify-start'" | |
| > | |
| <!-- User Bubble --> | |
| <div | |
| v-if="msg.role === 'user'" | |
| class="max-w-[85%] p-4 rounded-2xl shadow-sm text-sm leading-relaxed border transition-all bg-green-50/50 border-green-100 text-slate-800 rounded-tr-sm" | |
| > | |
| <div | |
| class="prose prose-sm max-w-none text-slate-800 font-medium prose-p:mb-0 last:prose-p:mb-0 prose-code:bg-slate-200/60 prose-code:text-slate-800 prose-code:px-1 prose-code:py-0.5 prose-code:rounded" | |
| v-html="renderMarkdown(msg.content)" | |
| ></div> | |
| </div> | |
| <!-- AI Message - Clean text, no avatar, no bubble --> | |
| <div v-else class="w-full transition-all duration-200"> | |
| <div | |
| class="prose prose-sm max-w-none prose-p:mb-4 last:prose-p:mb-0 prose-p:text-slate-700 prose-p:leading-7 prose-headings:text-slate-900 prose-headings:font-bold prose-headings:mb-3 prose-headings:mt-6 first:prose-headings:mt-0 prose-h2:text-base prose-h2:border-b prose-h2:border-slate-200 prose-h2:pb-2 prose-h3:text-sm prose-h3:text-slate-900 prose-h3:font-medium prose-h4:text-sm prose-h4:text-slate-800 prose-h4:font-medium prose-strong:text-slate-900 prose-strong:font-semibold prose-ul:list-disc prose-ul:pl-5 prose-ul:mb-4 prose-ul:space-y-1.5 prose-li:text-slate-700 prose-ol:list-decimal prose-ol:pl-5 prose-ol:mb-4 prose-ol:space-y-1.5 prose-li:text-slate-700 prose-code:bg-slate-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-emerald-700 prose-code:font-mono prose-code:text-[0.85em] prose-code:before:content-none prose-code:after:content-none prose-pre:bg-[#0f172a] prose-pre:text-green-400 prose-pre:border prose-pre:border-slate-800 prose-pre:rounded-lg prose-pre:p-4 prose-pre:mt-4 prose-pre:mb-4 prose-pre:overflow-x-auto prose-blockquote:border-l-2 prose-blockquote:border-emerald-300 prose-blockquote:bg-emerald-50/30 prose-blockquote:pl-4 prose-blockquote:pr-4 prose-blockquote:py-3 prose-blockquote:rounded-r-xl prose-blockquote:italic prose-blockquote:text-slate-600 prose-blockquote:my-4 prose-table:w-full prose-table:border-collapse prose-table:my-4 prose-th:bg-slate-50 prose-th:border prose-th:border-slate-200 prose-th:px-3 prose-th:py-2 prose-th:text-left prose-th:text-xs prose-th:font-bold prose-th:text-slate-700 prose-td:border prose-td:border-slate-200 prose-td:px-3 prose-td:py-2 prose-td:text-sm prose-td:text-slate-700 prose-hr:border-slate-200 prose-hr:my-6 prose-a:text-emerald-600 prose-a:underline prose-a:decoration-emerald-600/30 prose-a:underline-offset-2 hover:prose-a:decoration-emerald-600/60 space-y-2" | |
| v-html="renderMarkdown(msg.content)" | |
| ></div> | |
| </div> | |
| </div> | |
| <!-- Loading Indicator --> | |
| <div v-if="isTyping" class="flex justify-start animate-fade-in"> | |
| <div | |
| class="bg-green-50 border border-green-100 rounded-2xl rounded-tl-sm px-4 py-3 flex items-center gap-1.5 shadow-sm" | |
| > | |
| <span | |
| class="w-1.5 h-1.5 bg-green-400 rounded-full animate-bounce" | |
| ></span> | |
| <span | |
| class="w-1.5 h-1.5 bg-green-400 rounded-full animate-bounce" | |
| style="animation-delay: 0.15s" | |
| ></span> | |
| <span | |
| class="w-1.5 h-1.5 bg-green-400 rounded-full animate-bounce" | |
| style="animation-delay: 0.3s" | |
| ></span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="bg-white/80 border-t border-slate-100 shrink-0"> | |
| <div class="p-4"> | |
| <form @submit.prevent="sendMessage" class="relative group"> | |
| <input | |
| v-model="newMessage" | |
| type="text" | |
| @keydown.enter.prevent="sendMessage" | |
| :placeholder=" | |
| projectStore.loadingModels | |
| ? 'Loading AI models...' | |
| : 'Ask a question...' | |
| " | |
| class="w-full premium-input rounded-xl pl-4 pr-12 py-3 text-sm outline-none disabled:opacity-50 disabled:cursor-not-allowed" | |
| :disabled="isTyping || projectStore.loadingModels" | |
| /> | |
| <button | |
| type="submit" | |
| :disabled=" | |
| !newMessage.trim() || isTyping || projectStore.loadingModels | |
| " | |
| class="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 flex items-center justify-center rounded-lg bg-green-500 text-white hover:bg-green-600 disabled:opacity-50 disabled:bg-slate-300 transition-colors" | |
| :title=" | |
| projectStore.loadingModels ? 'Models loading' : 'Send message' | |
| " | |
| > | |
| <UIcon | |
| :name=" | |
| projectStore.loadingModels | |
| ? 'i-lucide-loader-2' | |
| : 'i-lucide-send' | |
| " | |
| class="w-4 h-4" | |
| :class=" | |
| projectStore.loadingModels | |
| ? 'animate-spin' | |
| : '-translate-x-[1px] translate-y-[1px]' | |
| " | |
| /> | |
| </button> | |
| </form> | |
| <div class="mt-2 text-[10px] text-center text-slate-400"> | |
| AlgoVision can make mistakes. Check important information. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { ref, onMounted, watch, nextTick, computed } from "vue"; | |
| import { marked } from "marked"; | |
| import markedKatex from "marked-katex-extension"; | |
| import "katex/dist/katex.min.css"; | |
| import api from "~/utils/api"; | |
| import { useProjectStore } from "~/stores/projects"; | |
| import { useChatStore } from "~/stores/chat"; | |
| marked.use( | |
| markedKatex({ | |
| throwOnError: false, | |
| output: "html", | |
| }), | |
| ); | |
| const props = defineProps({ | |
| project: { type: Object, required: true }, | |
| }); | |
| const projectStore = useProjectStore(); | |
| const chatStore = useChatStore(); | |
| const messages = computed(() => chatStore.getHistory(props.project.id)); | |
| const suggestions = computed(() => chatStore.getSuggestions(props.project.id)); | |
| const isTyping = ref(false); | |
| const newMessage = ref(""); | |
| const chatContainer = ref(null); | |
| const fetchSuggestions = async () => { | |
| try { | |
| await chatStore.fetchSuggestions(props.project.id, projectStore.apiKeys); | |
| } catch (err) { | |
| console.error("Error fetching suggestions:", err); | |
| } | |
| }; | |
| const useSuggestion = (text) => { | |
| newMessage.value = text; | |
| sendMessage(); | |
| }; | |
| const scrollToBottom = async () => { | |
| await nextTick(); | |
| if (chatContainer.value) { | |
| chatContainer.value.scrollTop = chatContainer.value.scrollHeight; | |
| } | |
| }; | |
| onMounted(() => { | |
| scrollToBottom(); | |
| if (!projectStore.loadingModels) { | |
| fetchSuggestions(); | |
| } | |
| }); | |
| watch( | |
| () => projectStore.loadingModels, | |
| (loading) => { | |
| if (!loading && suggestions.value.length === 0) { | |
| fetchSuggestions(); | |
| } | |
| }, | |
| ); | |
| watch(messages, scrollToBottom, { deep: true }); | |
| watch(isTyping, scrollToBottom); | |
| const renderMarkdown = (text) => { | |
| if (!text) return ""; | |
| try { | |
| return marked.parse(text); | |
| } catch (e) { | |
| return text; | |
| } | |
| }; | |
| const sendMessage = async () => { | |
| if (!newMessage.value.trim() || isTyping.value) return; | |
| const userText = newMessage.value; | |
| chatStore.addMessage(props.project.id, { role: "user", content: userText }); | |
| newMessage.value = ""; | |
| isTyping.value = true; | |
| try { | |
| const res = await api.chat(props.project.id, { | |
| message: userText, | |
| history: messages.value.slice(0, -1), | |
| model: | |
| projectStore.planningModel || | |
| projectStore.selectedModel || | |
| "gemini/gemini-3.1-flash-latest", | |
| openai_api_key: projectStore.apiKeys.openai, | |
| anthropic_api_key: projectStore.apiKeys.anthropic, | |
| gemini_api_key: projectStore.apiKeys.gemini, | |
| }); | |
| chatStore.addMessage(props.project.id, { | |
| role: "assistant", | |
| content: res.response, | |
| }); | |
| } catch (error) { | |
| console.error("Chat error:", error); | |
| chatStore.addMessage(props.project.id, { | |
| role: "assistant", | |
| content: `**Error:** Failed to connect to the AI engine. ${error.response?.data?.detail || error.message}`, | |
| }); | |
| } finally { | |
| isTyping.value = false; | |
| } | |
| }; | |
| </script> | |
| <style scoped> | |
| .custom-scrollbar::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb { | |
| background: rgba(203, 213, 225, 0.5); | |
| border-radius: 2px; | |
| } | |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { | |
| background: rgba(148, 163, 184, 0.8); | |
| } | |
| /* Additional prose polish for AI responses */ | |
| :deep(.prose a) { | |
| color: #059669; | |
| text-decoration: underline; | |
| text-decoration-color: rgba(5, 150, 105, 0.3); | |
| text-underline-offset: 2px; | |
| transition: all 0.15s ease; | |
| } | |
| :deep(.prose a:hover) { | |
| color: #047857; | |
| text-decoration-color: rgba(5, 150, 105, 0.6); | |
| } | |
| /* Inline math/latex */ | |
| :deep(.prose .katex) { | |
| font-size: 0.95em; | |
| } | |
| /* Display math blocks */ | |
| :deep(.prose .katex-display) { | |
| margin: 0.75rem 0; | |
| padding: 0.75rem; | |
| overflow-x: auto; | |
| } | |
| /* Smooth text rendering */ | |
| :deep(.prose *) { | |
| -webkit-font-smoothing: antialiased; | |
| -moz-osx-font-smoothing: grayscale; | |
| } | |
| /* Code block line numbers area (if any) */ | |
| :deep(.prose pre code) { | |
| font-family: | |
| ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", | |
| monospace; | |
| } | |
| /* Prevent code wrap */ | |
| :deep(.prose pre) { | |
| white-space: pre; | |
| word-break: normal; | |
| overflow-wrap: normal; | |
| } | |
| /* Table responsiveness */ | |
| :deep(.prose table) { | |
| display: table; | |
| width: 100%; | |
| overflow-x: auto; | |
| } | |
| </style> | |