feat(lms-web): improve AI guide workspace

This commit is contained in:
2026-07-20 16:39:26 +09:00
parent 9bce966404
commit 107ccd151e
3 changed files with 31 additions and 40 deletions
+27 -36
View File
@@ -1,4 +1,4 @@
import { BookOpen, ChevronRight, Code2, LoaderCircle, MessageSquareText, Plus, Sparkles, Trash2, X } from 'lucide-react' import { BookOpen, ChevronRight, Code2, LoaderCircle, MessageCircleQuestion, MessageSquareText, Plus, Sparkles, Trash2, X } from 'lucide-react'
import type React from 'react' import type React from 'react'
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { ExpandableMarkdownMessage, QaInputBox } from '@lms-renderer/components/ChatWidgets' import { ExpandableMarkdownMessage, QaInputBox } from '@lms-renderer/components/ChatWidgets'
@@ -93,8 +93,8 @@ export default function AiGuideView({
} }
return ( return (
<section className="relative flex h-full min-h-0 w-full flex-col overflow-hidden bg-[#202020] text-white"> <section className="relative flex h-full min-h-0 w-full flex-col overflow-hidden bg-[#101827] text-white">
<header className="flex h-10 shrink-0 items-center justify-between border-b border-white/10 bg-[#252525] px-3"> <header className="flex h-10 shrink-0 items-center justify-between border-b border-white/10 bg-[#17243b] px-3">
<strong className="truncate pl-4 text-[13px] font-bold text-white/90">AI 가이드</strong> <strong className="truncate pl-4 text-[13px] font-bold text-white/90">AI 가이드</strong>
<button <button
type="button" type="button"
@@ -108,7 +108,7 @@ export default function AiGuideView({
</header> </header>
<div className="flex min-h-0 flex-1"> <div className="flex min-h-0 flex-1">
<aside className="flex w-[252px] shrink-0 flex-col border-r border-white/10 bg-[#202228]"> <aside className="flex w-[252px] shrink-0 flex-col border-r border-white/10 bg-[#14213a]">
<div className="session-scrollbar min-h-0 flex-1 overflow-y-auto p-3"> <div className="session-scrollbar min-h-0 flex-1 overflow-y-auto p-3">
<> <>
<div className="rounded-lg border border-[#3b7ffa]/25 bg-[#3b7ffa]/[0.08] px-3 pb-3 pt-2"> <div className="rounded-lg border border-[#3b7ffa]/25 bg-[#3b7ffa]/[0.08] px-3 pb-3 pt-2">
@@ -124,7 +124,7 @@ export default function AiGuideView({
className="group mt-3 flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-[12px] font-semibold text-white/58 transition hover:bg-white/[0.07] hover:text-white disabled:cursor-not-allowed disabled:opacity-35" className="group mt-3 flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-[12px] font-semibold text-white/58 transition hover:bg-white/[0.07] hover:text-white disabled:cursor-not-allowed disabled:opacity-35"
> >
<Plus size={15} className="shrink-0 text-[#82abff]" /> <Plus size={15} className="shrink-0 text-[#82abff]" />
<span className="min-w-0 flex-1">새 대화</span> <span className="min-w-0 flex-1">새 채팅</span>
<ChevronRight size={14} className="shrink-0 opacity-0 transition group-hover:opacity-70" /> <ChevronRight size={14} className="shrink-0 opacity-0 transition group-hover:opacity-70" />
</button> </button>
@@ -149,26 +149,17 @@ export default function AiGuideView({
<div className="mt-6 border-t border-white/[0.08] pt-4"> <div className="mt-6 border-t border-white/[0.08] pt-4">
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<span className="text-[10px] font-bold tracking-[0.08em] text-white/38">최근 대화</span> <span className="text-[10px] font-bold tracking-[0.08em] text-white/38">최근 채팅</span>
</div> </div>
<div className="mt-2 grid gap-1"> <div className="mt-2 grid gap-1">
{activeConversation ? ( {activeConversation ? (
<button <button
type="button" type="button"
onClick={() => selectConversation(null)} onClick={() => selectConversation(null)}
className={['min-w-0 rounded-md px-2 py-2 text-left transition', !viewingHistory ? 'bg-[#3b7ffa]/16 text-[#c6d9ff]' : 'text-white/48 hover:bg-white/[0.06] hover:text-white/75'].join(' ')} className={['min-w-0 cursor-pointer rounded-md px-2 py-2 text-left transition', !viewingHistory ? 'bg-[#3b7ffa]/16 text-[#c6d9ff]' : 'text-white/48 hover:bg-white/[0.06] hover:text-white/75'].join(' ')}
> >
<span className="block truncate text-[11px] font-semibold">{activeConversation.title || '새 대화'}</span> <span className="block truncate text-[11px] font-semibold">{activeConversation.title || '새 채팅'}</span>
<span className="mt-0.5 block text-[10px] text-white/32">현재 대화</span> <span className="mt-0.5 block text-[10px] text-white/32">현재 채팅</span>
</button>
) : viewingHistory ? (
<button
type="button"
onClick={() => selectConversation(null)}
className="flex min-h-9 w-full cursor-pointer items-center gap-2 rounded-md px-2 text-left text-[11px] font-semibold text-white/48 transition hover:bg-white/[0.06] hover:text-white/75"
>
<MessageSquareText size={13} className="shrink-0" />
<span className="truncate">새 대화로 돌아가기</span>
</button> </button>
) : null} ) : null}
{history.map((thread) => ( {history.map((thread) => (
@@ -176,15 +167,15 @@ export default function AiGuideView({
<button <button
type="button" type="button"
onClick={() => selectConversation(thread.id)} onClick={() => selectConversation(thread.id)}
className="min-w-0 flex-1 px-2 py-2 text-left" className="min-w-0 flex-1 cursor-pointer px-2 py-2 text-left"
> >
<span className="block truncate text-[11px] font-semibold">{thread.title || '이전 대화'}</span> <span className="block truncate text-[11px] font-semibold">{thread.title || '이전 채팅'}</span>
<span className="mt-0.5 block text-[10px] text-white/32">{formatConversationDate(thread.lastMessageAt)}</span> <span className="mt-0.5 block text-[10px] text-white/32">{formatConversationDate(thread.lastMessageAt)}</span>
</button> </button>
<button <button
type="button" type="button"
title="대화 삭제" title="채팅 삭제"
aria-label="대화 삭제" aria-label="채팅 삭제"
onClick={() => setDeleteConfirmThread(thread)} onClick={() => setDeleteConfirmThread(thread)}
className="mr-1.5 grid size-6 shrink-0 cursor-pointer place-items-center rounded text-white/30 opacity-0 transition hover:bg-white/[0.08] hover:text-[#f87171] group-hover:opacity-100" className="mr-1.5 grid size-6 shrink-0 cursor-pointer place-items-center rounded text-white/30 opacity-0 transition hover:bg-white/[0.08] hover:text-[#f87171] group-hover:opacity-100"
> >
@@ -200,10 +191,10 @@ export default function AiGuideView({
</aside> </aside>
<main className="relative flex min-w-0 flex-1 flex-col bg-[#181a20]"> <main className="relative flex min-w-0 flex-1 flex-col bg-[#0d1524]">
<div ref={conversationScrollRef} className="session-scrollbar min-h-0 flex-1 overflow-y-auto px-6 pb-28 pt-7"> <div ref={conversationScrollRef} className="session-scrollbar min-h-0 flex-1 overflow-y-auto px-6 pb-28 pt-7">
{loading ? ( {loading ? (
<div className="grid h-full min-h-[300px] place-items-center text-[12px] font-semibold text-white/40"><span className="flex items-center gap-2"><LoaderCircle size={15} className="animate-spin" /> 이전 대화를 불러오는 중</span></div> <div className="grid h-full min-h-[300px] place-items-center text-[12px] font-semibold text-white/40"><span className="flex items-center gap-2"><LoaderCircle size={15} className="animate-spin" /> 이전 채팅을 불러오는 중</span></div>
) : disabled ? ( ) : disabled ? (
<div className="grid h-full min-h-[300px] place-items-center text-center"> <div className="grid h-full min-h-[300px] place-items-center text-center">
<div> <div>
@@ -229,7 +220,7 @@ export default function AiGuideView({
> >
<div className={message.role === 'user' <div className={message.role === 'user'
? 'max-w-[78%] rounded-xl rounded-br-sm bg-[#3b7ffa] px-3.5 py-2.5 text-[12px] font-medium leading-relaxed text-white shadow-[0_8px_20px_rgba(32,91,204,0.24)]' ? 'max-w-[78%] rounded-xl rounded-br-sm bg-[#3b7ffa] px-3.5 py-2.5 text-[12px] font-medium leading-relaxed text-white shadow-[0_8px_20px_rgba(32,91,204,0.24)]'
: 'w-full rounded-xl rounded-tl-sm border border-white/10 bg-[#22252c] px-4 py-3'} : 'w-full rounded-xl rounded-tl-sm border border-white/10 bg-[#18253a] px-4 py-3'}
> >
{message.role === 'user' ? ( {message.role === 'user' ? (
<p className="whitespace-pre-wrap">{message.content}</p> <p className="whitespace-pre-wrap">{message.content}</p>
@@ -251,7 +242,7 @@ export default function AiGuideView({
))} ))}
{busy && ( {busy && (
<article className="w-full"> <article className="w-full">
<div className="inline-flex items-center gap-2 rounded-xl rounded-tl-sm border border-white/10 bg-[#22252c] px-4 py-3 text-[12px] font-semibold text-[#8fb4ff]"> <div className="inline-flex items-center gap-2 rounded-xl rounded-tl-sm border border-white/10 bg-[#18253a] px-4 py-3 text-[12px] font-semibold text-[#8fb4ff]">
<LoaderCircle size={15} className="animate-spin" /> 생각 중 <LoaderCircle size={15} className="animate-spin" /> 생각 중
</div> </div>
</article> </article>
@@ -264,15 +255,15 @@ export default function AiGuideView({
<div className="pointer-events-auto mx-auto max-w-[900px]"><QaInputBox dark compact busy={busy} disabled={disabled || viewingHistory || switchingConversation} onStop={onStop} onSubmit={onSubmit} /></div> <div className="pointer-events-auto mx-auto max-w-[900px]"><QaInputBox dark compact busy={busy} disabled={disabled || viewingHistory || switchingConversation} onStop={onStop} onSubmit={onSubmit} /></div>
</div> </div>
{switchingConversation && ( {switchingConversation && (
<div className="absolute inset-0 z-20 grid place-items-center bg-[#181a20]/78 backdrop-blur-[2px]"> <div className="absolute inset-0 z-20 grid place-items-center bg-[#0d1524]/78 backdrop-blur-[2px]">
<span className="flex items-center gap-2 rounded-lg border border-white/10 bg-[#252525] px-4 py-3 text-[12px] font-semibold text-white/76 shadow-[0_16px_32px_rgba(0,0,0,0.28)]"><LoaderCircle size={15} className="animate-spin text-[#8fb4ff]" /> 대화 불러오는 중</span> <span className="flex items-center gap-2 rounded-lg border border-white/10 bg-[#1b2a42] px-4 py-3 text-[12px] font-semibold text-white/76 shadow-[0_16px_32px_rgba(0,0,0,0.28)]"><LoaderCircle size={15} className="animate-spin text-[#8fb4ff]" /> 채팅 불러오는 중</span>
</div> </div>
)} )}
</main> </main>
<aside className="flex w-[232px] shrink-0 flex-col border-l border-white/10 bg-[#202228]"> <aside className="flex w-[232px] shrink-0 flex-col border-l border-white/10 bg-[#14213a]">
<div className="flex h-10 shrink-0 items-center border-b border-white/10 px-3"> <div className="flex h-10 shrink-0 items-center border-b border-white/10 px-3">
<span className="text-[11px] font-bold tracking-[0.08em] text-white/44">질문 목록</span> <span className="flex items-center gap-1.5 text-[11px] font-bold tracking-[0.08em] text-white/44"><MessageCircleQuestion size={13} />질문 목록</span>
</div> </div>
<div className="session-scrollbar min-h-0 flex-1 overflow-y-auto p-2"> <div className="session-scrollbar min-h-0 flex-1 overflow-y-auto p-2">
{questionMessages.length ? ( {questionMessages.length ? (
@@ -282,7 +273,7 @@ export default function AiGuideView({
key={message.id} key={message.id}
type="button" type="button"
onClick={() => scrollToQuestion(message.id)} onClick={() => scrollToQuestion(message.id)}
className={['w-full min-w-0 overflow-hidden rounded-md px-2.5 py-2 text-left transition', selectedQuestionId === message.id ? 'bg-[#3b7ffa]/16 text-[#c6d9ff]' : 'text-white/52 hover:bg-white/[0.06] hover:text-white/78'].join(' ')} className={['w-full min-w-0 cursor-pointer overflow-hidden rounded-md px-2.5 py-2 text-left transition', selectedQuestionId === message.id ? 'bg-[#3b7ffa]/16 text-[#c6d9ff]' : 'text-white/52 hover:bg-white/[0.06] hover:text-white/78'].join(' ')}
> >
<span className="flex w-full min-w-0 items-start gap-2"> <span className="flex w-full min-w-0 items-start gap-2">
<span className="min-w-0 flex-1 truncate text-[11px] font-medium leading-relaxed">{message.content}</span> <span className="min-w-0 flex-1 truncate text-[11px] font-medium leading-relaxed">{message.content}</span>
@@ -299,15 +290,15 @@ export default function AiGuideView({
</div> </div>
{deleteConfirmThread && ( {deleteConfirmThread && (
<div className="absolute inset-0 z-50 grid place-items-center bg-black/36"> <div className="absolute inset-0 z-50 grid place-items-center bg-black/36">
<section className="w-[360px] overflow-hidden rounded-lg border border-white/10 bg-[#252525] text-white shadow-[0_24px_70px_rgba(0,0,0,0.42)]"> <section className="w-[360px] overflow-hidden rounded-lg border border-white/10 bg-[#1b2a42] text-white shadow-[0_24px_70px_rgba(0,0,0,0.42)]">
<div className="flex items-center gap-2 border-b border-white/10 px-4 py-3"> <div className="flex items-center gap-2 border-b border-white/10 px-4 py-3">
<Trash2 size={15} className="text-[#e87979]" /> <Trash2 size={15} className="text-[#e87979]" />
<strong className="text-[13px] font-bold">대화 삭제</strong> <strong className="text-[13px] font-bold">채팅 삭제</strong>
</div> </div>
<div className="p-4"> <div className="p-4">
<p className="text-[13px] font-semibold text-white">이 대화 내역을 삭제할까요?</p> <p className="text-[13px] font-semibold text-white">이 채팅 내역을 삭제할까요?</p>
<p className="mt-2 truncate text-[12px] font-medium text-white/52">{deleteConfirmThread.title || '이전 대화'}</p> <p className="mt-2 truncate text-[12px] font-medium text-white/52">{deleteConfirmThread.title || '이전 채팅'}</p>
<p className="mt-2 text-[12px] font-medium leading-relaxed text-white/45">삭제한 대화와 답변은 복구할 수 없습니다.</p> <p className="mt-2 text-[12px] font-medium leading-relaxed text-white/45">삭제한 채팅과 답변은 복구할 수 없습니다.</p>
<div className="mt-4 flex justify-end gap-2"> <div className="mt-4 flex justify-end gap-2">
<button <button
type="button" type="button"
+1 -1
View File
@@ -2671,7 +2671,7 @@ function LocalPcWorkspaceMockup({
if (mode === 'teacher') setTeacherChatFlash(false) if (mode === 'teacher') setTeacherChatFlash(false)
}} }}
onStop={stopAiResponse} onStop={stopAiResponse}
onSubmit={(content) => void sendQaMessage(content)} onSubmit={(content) => void sendQaMessage(content, { contextWeek: selectedWeekState?.week })}
onClearConversation={() => { onClearConversation={() => {
void clearQaConversation().catch((error) => console.error('[chat] clear failed', error)) void clearQaConversation().catch((error) => console.error('[chat] clear failed', error))
}} }}
+3 -3
View File
@@ -55,7 +55,7 @@ export default function Sidebar({
return ( return (
<aside <aside
className={[ className={[
'flex shrink-0 flex-col rounded-bl-[18px] bg-[#181b20] px-2.5 pb-3.5 pt-[18px] transition-[width] duration-200 ease-out', 'flex shrink-0 flex-col rounded-bl-[18px] bg-[#111520] px-2.5 pb-3.5 pt-[18px] transition-[width] duration-200 ease-out',
collapsed ? 'w-[68px]' : 'w-[240px]' collapsed ? 'w-[68px]' : 'w-[240px]'
].join(' ')} ].join(' ')}
> >
@@ -177,11 +177,11 @@ export default function Sidebar({
className="relative grid h-[34px] w-full cursor-pointer place-items-center rounded bg-transparent text-white/75 transition hover:bg-white/[0.06]" className="relative grid h-[34px] w-full cursor-pointer place-items-center rounded bg-transparent text-white/75 transition hover:bg-white/[0.06]"
> >
<Activity size={17} /> <Activity size={17} />
<span className={['absolute right-2 top-1 h-2.5 w-2.5 rounded-full ring-2 ring-[#181b20]', visibleRunningSessions[0]?.dotClassName ?? 'bg-[#14b8a6]'].join(' ')} /> <span className={['absolute right-2 top-1 h-2.5 w-2.5 rounded-full ring-2 ring-[#111520]', visibleRunningSessions[0]?.dotClassName ?? 'bg-[#14b8a6]'].join(' ')} />
</button> </button>
)} )}
{collapsed && sessionPopoverOpen && ( {collapsed && sessionPopoverOpen && (
<div className="absolute bottom-0 left-[62px] z-20 w-[240px] rounded-md border border-white/10 bg-[#181b20] py-1.5 shadow-[18px_0_42px_rgba(0,0,0,0.22)]"> <div className="absolute bottom-0 left-[62px] z-20 w-[240px] rounded-md border border-white/10 bg-[#111520] py-1.5 shadow-[18px_0_42px_rgba(0,0,0,0.22)]">
<div className="session-scrollbar max-h-[286px] overflow-y-auto pr-1"> <div className="session-scrollbar max-h-[286px] overflow-y-auto pr-1">
{visibleRunningSessions.map((session, index) => ( {visibleRunningSessions.map((session, index) => (
<div key={session.id} className="relative border-b border-white/10 last:border-b-0"> <div key={session.id} className="relative border-b border-white/10 last:border-b-0">