feat(lms-web): improve AI guide workspace
This commit is contained in:
@@ -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 { useRef, useState } from 'react'
|
||||
import { ExpandableMarkdownMessage, QaInputBox } from '@lms-renderer/components/ChatWidgets'
|
||||
@@ -93,8 +93,8 @@ export default function AiGuideView({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="relative flex h-full min-h-0 w-full flex-col overflow-hidden bg-[#202020] text-white">
|
||||
<header className="flex h-10 shrink-0 items-center justify-between border-b border-white/10 bg-[#252525] px-3">
|
||||
<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-[#17243b] px-3">
|
||||
<strong className="truncate pl-4 text-[13px] font-bold text-white/90">AI 가이드</strong>
|
||||
<button
|
||||
type="button"
|
||||
@@ -108,7 +108,7 @@ export default function AiGuideView({
|
||||
</header>
|
||||
|
||||
<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="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"
|
||||
>
|
||||
<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" />
|
||||
</button>
|
||||
|
||||
@@ -149,26 +149,17 @@ export default function AiGuideView({
|
||||
|
||||
<div className="mt-6 border-t border-white/[0.08] pt-4">
|
||||
<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 className="mt-2 grid gap-1">
|
||||
{activeConversation ? (
|
||||
<button
|
||||
type="button"
|
||||
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="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>
|
||||
<span className="block truncate text-[11px] font-semibold">{activeConversation.title || '새 채팅'}</span>
|
||||
<span className="mt-0.5 block text-[10px] text-white/32">현재 채팅</span>
|
||||
</button>
|
||||
) : null}
|
||||
{history.map((thread) => (
|
||||
@@ -176,15 +167,15 @@ export default function AiGuideView({
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
title="대화 삭제"
|
||||
aria-label="대화 삭제"
|
||||
title="채팅 삭제"
|
||||
aria-label="채팅 삭제"
|
||||
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"
|
||||
>
|
||||
@@ -200,10 +191,10 @@ export default function AiGuideView({
|
||||
|
||||
</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">
|
||||
{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 ? (
|
||||
<div className="grid h-full min-h-[300px] place-items-center text-center">
|
||||
<div>
|
||||
@@ -229,7 +220,7 @@ export default function AiGuideView({
|
||||
>
|
||||
<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)]'
|
||||
: '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' ? (
|
||||
<p className="whitespace-pre-wrap">{message.content}</p>
|
||||
@@ -251,7 +242,7 @@ export default function AiGuideView({
|
||||
))}
|
||||
{busy && (
|
||||
<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" /> 생각 중
|
||||
</div>
|
||||
</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>
|
||||
{switchingConversation && (
|
||||
<div className="absolute inset-0 z-20 grid place-items-center bg-[#181a20]/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>
|
||||
<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-[#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>
|
||||
)}
|
||||
</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">
|
||||
<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 className="session-scrollbar min-h-0 flex-1 overflow-y-auto p-2">
|
||||
{questionMessages.length ? (
|
||||
@@ -282,7 +273,7 @@ export default function AiGuideView({
|
||||
key={message.id}
|
||||
type="button"
|
||||
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="min-w-0 flex-1 truncate text-[11px] font-medium leading-relaxed">{message.content}</span>
|
||||
@@ -299,15 +290,15 @@ export default function AiGuideView({
|
||||
</div>
|
||||
{deleteConfirmThread && (
|
||||
<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">
|
||||
<Trash2 size={15} className="text-[#e87979]" />
|
||||
<strong className="text-[13px] font-bold">대화 삭제</strong>
|
||||
<strong className="text-[13px] font-bold">채팅 삭제</strong>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<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 text-[12px] font-medium leading-relaxed text-white/45">삭제한 대화와 답변은 복구할 수 없습니다.</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 text-[12px] font-medium leading-relaxed text-white/45">삭제한 채팅과 답변은 복구할 수 없습니다.</p>
|
||||
<div className="mt-4 flex justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -2671,7 +2671,7 @@ function LocalPcWorkspaceMockup({
|
||||
if (mode === 'teacher') setTeacherChatFlash(false)
|
||||
}}
|
||||
onStop={stopAiResponse}
|
||||
onSubmit={(content) => void sendQaMessage(content)}
|
||||
onSubmit={(content) => void sendQaMessage(content, { contextWeek: selectedWeekState?.week })}
|
||||
onClearConversation={() => {
|
||||
void clearQaConversation().catch((error) => console.error('[chat] clear failed', error))
|
||||
}}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function Sidebar({
|
||||
return (
|
||||
<aside
|
||||
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]'
|
||||
].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]"
|
||||
>
|
||||
<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>
|
||||
)}
|
||||
{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">
|
||||
{visibleRunningSessions.map((session, index) => (
|
||||
<div key={session.id} className="relative border-b border-white/10 last:border-b-0">
|
||||
|
||||
Reference in New Issue
Block a user