| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199 |
- 'use client';
- import { useEffect, useRef } from 'react';
- import { BookOpen, MessageSquare, Flashlight, MousePointer2, Play } from 'lucide-react';
- import { cn } from '@/lib/utils';
- import { useI18n } from '@/lib/hooks/use-i18n';
- import type { LectureNoteEntry } from '@/lib/types/chat';
- const ACTION_ICON_ONLY: Record<string, { Icon: typeof Flashlight; style: string }> = {
- spotlight: {
- Icon: Flashlight,
- style:
- 'bg-yellow-50 dark:bg-yellow-500/15 border-yellow-300/40 dark:border-yellow-500/30 text-yellow-700 dark:text-yellow-300',
- },
- laser: {
- Icon: MousePointer2,
- style:
- 'bg-red-50 dark:bg-red-500/15 border-red-300/40 dark:border-red-500/30 text-red-600 dark:text-red-300',
- },
- play_video: {
- Icon: Play,
- style:
- 'bg-yellow-50 dark:bg-yellow-500/15 border-yellow-300/40 dark:border-yellow-500/30 text-yellow-700 dark:text-yellow-300',
- },
- };
- interface LectureNotesViewProps {
- notes: LectureNoteEntry[];
- currentSceneId?: string | null;
- }
- export function LectureNotesView({ notes, currentSceneId }: LectureNotesViewProps) {
- const { t } = useI18n();
- const containerRef = useRef<HTMLDivElement>(null);
- // Auto-scroll to the current scene note
- useEffect(() => {
- if (!currentSceneId || !containerRef.current) return;
- const el = containerRef.current.querySelector(`[data-scene-id="${currentSceneId}"]`);
- if (el) {
- el.scrollIntoView({ behavior: 'smooth', block: 'center' });
- }
- }, [currentSceneId]);
- // Empty state
- if (notes.length === 0) {
- return (
- <div className="h-full flex flex-col items-center justify-center text-center p-6">
- <div className="w-12 h-12 bg-purple-50 dark:bg-purple-900/20 rounded-2xl flex items-center justify-center mb-3 text-purple-300 dark:text-purple-600 ring-1 ring-purple-100 dark:ring-purple-800/30">
- <BookOpen className="w-6 h-6" />
- </div>
- <p className="text-xs font-medium text-gray-500 dark:text-gray-400">
- {t('chat.lectureNotes.empty')}
- </p>
- <p className="text-[10px] text-gray-400 dark:text-gray-500 mt-1">
- {t('chat.lectureNotes.emptyHint')}
- </p>
- </div>
- );
- }
- return (
- <div
- ref={containerRef}
- className="flex-1 overflow-y-auto overflow-x-hidden px-3 py-2 scrollbar-hide"
- >
- {notes.map((note, index) => {
- const isCurrent = note.sceneId === currentSceneId;
- const pageNum = index + 1;
- const pageLabel = t('chat.lectureNotes.pageLabel', { n: pageNum });
- return (
- <div
- key={note.sceneId}
- data-scene-id={note.sceneId}
- className={cn(
- 'relative mb-3 last:mb-0 rounded-lg px-3 py-2.5 transition-colors duration-200',
- isCurrent
- ? 'bg-purple-50/80 dark:bg-purple-950/25 ring-1 ring-purple-200/60 dark:ring-purple-700/30'
- : 'bg-gray-50/50 dark:bg-gray-800/30',
- )}
- >
- {/* Page label row */}
- <div className="flex items-center gap-2 mb-1.5">
- {/* Timeline dot */}
- <div
- className={cn(
- 'w-2 h-2 rounded-full shrink-0',
- isCurrent
- ? 'bg-purple-500 dark:bg-purple-400 shadow-sm shadow-purple-400/40'
- : 'bg-gray-300 dark:bg-gray-600',
- )}
- />
- <span
- className={cn(
- 'text-[10px] font-semibold tracking-wide',
- isCurrent
- ? 'text-purple-600 dark:text-purple-400'
- : 'text-gray-400 dark:text-gray-500',
- )}
- >
- {pageLabel}
- </span>
- {isCurrent && (
- <span className="text-[9px] font-bold px-1.5 py-px rounded-full bg-purple-100 dark:bg-purple-900/40 text-purple-600 dark:text-purple-300">
- {t('chat.lectureNotes.currentPage')}
- </span>
- )}
- </div>
- {/* Scene title */}
- <h4 className="text-[13px] font-bold text-gray-800 dark:text-gray-100 mb-1.5 leading-snug pl-4">
- {note.sceneTitle}
- </h4>
- {/* Ordered items: spotlight/laser inline at sentence start, discussion as card */}
- <div className="pl-4 space-y-1">
- {(() => {
- // Build render rows: group inline actions (spotlight/laser) with next speech,
- // but render discussion as its own block
- type Row =
- | { kind: 'speech'; inlineActions: string[]; text: string }
- | { kind: 'discussion'; label?: string }
- | { kind: 'trailing'; inlineActions: string[] };
- const rows: Row[] = [];
- let pendingInline: string[] = [];
- for (const item of note.items) {
- if (item.kind === 'action' && item.type === 'discussion') {
- // Flush pending inline actions as trailing if any
- if (pendingInline.length > 0) {
- rows.push({
- kind: 'trailing',
- inlineActions: pendingInline,
- });
- pendingInline = [];
- }
- rows.push({ kind: 'discussion', label: item.label });
- } else if (item.kind === 'action') {
- pendingInline.push(item.type);
- } else {
- rows.push({
- kind: 'speech',
- inlineActions: pendingInline,
- text: item.text,
- });
- pendingInline = [];
- }
- }
- if (pendingInline.length > 0) {
- rows.push({ kind: 'trailing', inlineActions: pendingInline });
- }
- return rows.map((row, i) => {
- if (row.kind === 'discussion') {
- return (
- <div
- key={i}
- className="my-1.5 flex items-start gap-1.5 rounded-md border border-amber-200/60 dark:border-amber-700/30 bg-amber-50/60 dark:bg-amber-900/10 px-2 py-1.5"
- >
- <MessageSquare className="w-3 h-3 text-amber-500 dark:text-amber-400 shrink-0 mt-0.5" />
- <span className="text-[11px] leading-snug text-amber-800 dark:text-amber-300">
- {row.label}
- </span>
- </div>
- );
- }
- const actions = row.kind === 'trailing' ? row.inlineActions : row.inlineActions;
- return (
- <p
- key={i}
- className="text-[12px] leading-[1.8] text-gray-700 dark:text-gray-300"
- >
- {actions.map((a, j) => {
- const cfg = ACTION_ICON_ONLY[a];
- if (!cfg) return null;
- const { Icon, style } = cfg;
- return (
- <span
- key={j}
- className={cn(
- 'inline-flex items-center justify-center w-4 h-4 rounded-full border align-middle mr-0.5',
- style,
- )}
- >
- <Icon className="w-2.5 h-2.5" />
- </span>
- );
- })}
- {row.kind === 'speech' ? row.text : null}
- </p>
- );
- });
- })()}
- </div>
- </div>
- );
- })}
- </div>
- );
- }
|