Skip to content

Commit 92a4635

Browse files
committed
Videos Added 1.
Made-with: Cursor
1 parent 61a4a21 commit 92a4635

4 files changed

Lines changed: 197 additions & 129 deletions

File tree

src/components/ChatPanel.vue

Lines changed: 128 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, nextTick, onBeforeUnmount, computed } from "vue";
2+
import { ref, nextTick, onBeforeUnmount, watch } from "vue";
33
import { marked } from "marked";
44
import type { DemoQuestion } from "@/data/demos";
55
import QuestionButton from "@/components/QuestionButton.vue";
@@ -16,13 +16,27 @@ const props = defineProps<{
1616
1717
const messages = ref<ChatMessage[]>([]);
1818
const isTyping = ref(false);
19-
const usedQuestionIds = ref(new Set<string>());
2019
const chatContainer = ref<HTMLDivElement>();
20+
const currentQuestions = ref<DemoQuestion[]>([]);
21+
const hasStarted = ref(false);
22+
const activeQuestion = ref<DemoQuestion | null>(null);
2123
let typeTimer: ReturnType<typeof setInterval> | null = null;
2224
23-
const availableQuestions = computed(() =>
24-
props.questions.filter((q) => !usedQuestionIds.value.has(q.id))
25-
);
25+
watch(() => props.questions, () => {
26+
resetChat();
27+
}, { immediate: true });
28+
29+
function resetChat() {
30+
messages.value = [];
31+
isTyping.value = false;
32+
hasStarted.value = false;
33+
activeQuestion.value = null;
34+
currentQuestions.value = [...props.questions];
35+
if (typeTimer) {
36+
clearInterval(typeTimer);
37+
typeTimer = null;
38+
}
39+
}
2640
2741
function scrollToBottom() {
2842
nextTick(() => {
@@ -39,7 +53,10 @@ function renderMarkdown(text: string): string {
3953
function askQuestion(question: DemoQuestion) {
4054
if (isTyping.value) return;
4155
42-
usedQuestionIds.value.add(question.id);
56+
hasStarted.value = true;
57+
activeQuestion.value = question;
58+
// Hide current questions immediately
59+
currentQuestions.value = [];
4360
4461
messages.value.push({
4562
role: "user",
@@ -48,36 +65,57 @@ function askQuestion(question: DemoQuestion) {
4865
4966
scrollToBottom();
5067
51-
const assistantMsg: ChatMessage = {
68+
messages.value.push({
5269
role: "assistant",
5370
content: "",
5471
isTyping: true,
55-
};
56-
messages.value.push(assistantMsg);
72+
});
73+
// Get the reactive proxy reference — mutating this triggers Vue re-renders
74+
const reactiveMsg = messages.value[messages.value.length - 1];
5775
isTyping.value = true;
5876
5977
let charIndex = 0;
6078
const answer = question.answer;
61-
const CHAR_DELAY_MS = 25; // slightly slower typing for realistic effect
79+
const CHAR_DELAY_MS = 25;
6280
6381
typeTimer = setInterval(() => {
6482
if (charIndex < answer.length) {
6583
let step = 1;
6684
if (answer[charIndex] === "\n") step = 2;
67-
assistantMsg.content = answer.slice(0, charIndex + step);
85+
reactiveMsg.content = answer.slice(0, charIndex + step);
6886
charIndex += step;
6987
scrollToBottom();
7088
} else {
71-
assistantMsg.isTyping = false;
72-
isTyping.value = false;
73-
if (typeTimer) {
74-
clearInterval(typeTimer);
75-
typeTimer = null;
76-
}
89+
finishTyping(reactiveMsg, question);
7790
}
7891
}, CHAR_DELAY_MS);
7992
}
8093
94+
function stopTyping() {
95+
if (typeTimer) {
96+
clearInterval(typeTimer);
97+
typeTimer = null;
98+
}
99+
const lastMsg = messages.value[messages.value.length - 1];
100+
if (lastMsg && lastMsg.isTyping && activeQuestion.value) {
101+
lastMsg.content = activeQuestion.value.answer;
102+
finishTyping(lastMsg, activeQuestion.value);
103+
scrollToBottom();
104+
}
105+
}
106+
107+
function finishTyping(msg: ChatMessage, question: DemoQuestion) {
108+
msg.isTyping = false;
109+
isTyping.value = false;
110+
if (typeTimer) {
111+
clearInterval(typeTimer);
112+
typeTimer = null;
113+
}
114+
if (question.followUps && question.followUps.length > 0) {
115+
currentQuestions.value = [...question.followUps];
116+
}
117+
}
118+
81119
onBeforeUnmount(() => {
82120
if (typeTimer) clearInterval(typeTimer);
83121
});
@@ -161,27 +199,80 @@ defineExpose({ askQuestion });
161199
</div>
162200
</div>
163201

164-
<!-- Question buttons floating at the bottom -->
165-
<div class="border-t border-emerald-100 bg-white/70 backdrop-blur-md px-5 py-4 shadow-[0_-4px_10px_rgba(0,0,0,0.02)]">
166-
<p class="mb-3 text-xs font-bold tracking-widest uppercase text-emerald-800/60">
167-
Suggested questions
168-
</p>
169-
<div class="flex flex-wrap gap-2.5">
170-
<QuestionButton
171-
v-for="q in questions"
172-
:key="q.id"
173-
:text="q.text"
174-
:disabled="isTyping"
175-
:used="usedQuestionIds.has(q.id)"
176-
@click="askQuestion(q)"
177-
/>
202+
<!-- Action Area (Questions & Controls) floating at the bottom -->
203+
<div class="border-t border-emerald-100 bg-white/70 backdrop-blur-md px-5 py-4 shadow-[0_-4px_10px_rgba(0,0,0,0.02)] min-h-[96px] flex flex-col justify-center">
204+
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
205+
206+
<!-- Left: Questions or Status -->
207+
<div class="flex-1">
208+
<div v-if="!hasStarted">
209+
<p class="mb-3 text-xs font-bold tracking-widest uppercase text-emerald-800/60">
210+
Suggested questions
211+
</p>
212+
<div class="flex flex-wrap gap-2.5">
213+
<QuestionButton
214+
v-for="q in currentQuestions"
215+
:key="q.id"
216+
:text="q.text"
217+
:disabled="false"
218+
:used="false"
219+
@click="askQuestion(q)"
220+
/>
221+
</div>
222+
</div>
223+
224+
<div v-else>
225+
<div v-if="isTyping" class="flex items-center gap-2 text-sm font-medium text-emerald-600">
226+
<span class="relative flex h-3 w-3">
227+
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
228+
<span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
229+
</span>
230+
Model is typing...
231+
</div>
232+
233+
<div v-else-if="currentQuestions.length > 0">
234+
<p class="mb-3 text-xs font-bold tracking-widest uppercase text-emerald-800/60">
235+
Follow-up questions
236+
</p>
237+
<div class="flex flex-wrap gap-2.5">
238+
<QuestionButton
239+
v-for="q in currentQuestions"
240+
:key="q.id"
241+
:text="q.text"
242+
:disabled="false"
243+
:used="false"
244+
@click="askQuestion(q)"
245+
/>
246+
</div>
247+
</div>
248+
249+
<div v-else class="text-sm font-medium text-gray-500">
250+
Dialogue complete. Return to the gallery or clear chat to try again.
251+
</div>
252+
</div>
253+
</div>
254+
255+
<!-- Right: Stop/Clear Buttons -->
256+
<div v-if="hasStarted" class="flex shrink-0 gap-2 items-end justify-end mt-2 sm:mt-0">
257+
<button
258+
v-if="isTyping"
259+
@click="stopTyping"
260+
class="flex items-center gap-1.5 rounded-2xl border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-bold text-red-600 shadow-sm transition-all hover:bg-red-100 hover:shadow"
261+
>
262+
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20"><rect x="5" y="5" width="10" height="10" rx="2" /></svg>
263+
Stop
264+
</button>
265+
<button
266+
v-if="!isTyping"
267+
@click="resetChat"
268+
class="flex items-center gap-1.5 rounded-2xl border border-gray-200 bg-white px-4 py-2.5 text-sm font-bold text-gray-600 shadow-sm transition-all hover:bg-gray-50 hover:shadow"
269+
>
270+
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
271+
Clear
272+
</button>
273+
</div>
274+
178275
</div>
179-
<p
180-
v-if="availableQuestions.length === 0 && !isTyping"
181-
class="mt-3 text-sm font-medium text-gray-500"
182-
>
183-
All questions explored. Return to the gallery to try another video.
184-
</p>
185276
</div>
186277
</div>
187278
</template>

src/components/VideoCard.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,17 @@ defineProps<{
1111
:to="{ name: 'demo', params: { id: video.id } }"
1212
class="group block overflow-hidden rounded-2xl border border-emerald-100 bg-white/80 backdrop-blur-sm transition-all duration-300 hover:border-emerald-300 hover:shadow-xl hover:shadow-emerald-900/10 hover:-translate-y-1"
1313
>
14-
<div class="relative aspect-video overflow-hidden">
14+
<div class="relative aspect-video overflow-hidden bg-emerald-900/10">
15+
<!-- Use video element for thumbnail when no image is provided -->
16+
<video
17+
v-if="!video.thumbnailUrl"
18+
:src="video.videoUrl"
19+
preload="metadata"
20+
muted
21+
class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"
22+
/>
1523
<img
24+
v-else
1625
:src="video.thumbnailUrl"
1726
:alt="video.title"
1827
class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"

src/components/VideoPlayer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ defineProps<{
1010
<div class="overflow-hidden rounded-2xl border border-emerald-200 bg-black shadow-lg">
1111
<video
1212
:src="src"
13-
:poster="poster"
13+
:poster="poster || undefined"
1414
controls
1515
preload="metadata"
1616
class="aspect-video w-full bg-black object-contain"

0 commit comments

Comments
 (0)