Skip to content

Commit 636c208

Browse files
committed
Phase 2 완료: Chat UI 리디자인, LMS 학습 도구 4종, 컨텍스트 자동 첨부, 헬스 패널
Chat UI: - ChatMessageList: 마크다운 렌더링, 복사 버튼, 타이핑 애니메이션, 접이식 도구 카드 - ChatInput: 슬래시 커맨드 6종 (/learn, /quiz, /explain, /debug, /review, /report) LMS 도구: - create-learning-card: 개념 카드 (설명 + 예시 + 빈칸 채우기) - create-quiz: 퀴즈 생성 (객관식 / 코딩 / 출력 예측) - create-notebook-exercise: 다단계 연습 (fill-blank → modify → write) - track-achievement: 학습 성취 기록 (.codaro/achievements.json) 컨텍스트 자동 첨부: - registerContextProvider로 노트북 상태 자동 주입 - 선택된 셀, 변수, 블록, 파일명을 메시지에 첨부 - 백엔드 _injectContext가 chat/stream 양쪽 지원 헬스 패널: HealthPanel.svelte (세션/대화/엔진/메모리 모니터링)
1 parent 094904f commit 636c208

9 files changed

Lines changed: 1349 additions & 177 deletions

File tree

editor/src/lib/codaro/ai/ChatInput.svelte

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
<script lang="ts">
2-
import { Send, Loader2 } from "lucide-svelte";
2+
import { Send, Loader2, Slash } from "lucide-svelte";
3+
4+
interface SlashCommand {
5+
name: string;
6+
description: string;
7+
prefix: string;
8+
}
9+
10+
const slashCommands: SlashCommand[] = [
11+
{ name: "learn", description: "Start a learning session on a topic", prefix: "/learn " },
12+
{ name: "quiz", description: "Generate a quiz on the current topic", prefix: "/quiz " },
13+
{ name: "explain", description: "Explain a concept or code block", prefix: "/explain " },
14+
{ name: "debug", description: "Debug the current code or error", prefix: "/debug " },
15+
{ name: "review", description: "Review code for improvements", prefix: "/review " },
16+
{ name: "report", description: "Generate a report from results", prefix: "/report " },
17+
];
318
419
interface Props {
520
ready?: boolean;
@@ -19,6 +34,9 @@
1934
2035
let inputValue = $state("");
2136
let textareaEl: HTMLTextAreaElement | undefined = $state();
37+
let showSlashMenu = $state(false);
38+
let filteredCommands = $state<SlashCommand[]>([]);
39+
let selectedCommandIdx = $state(0);
2240
2341
let maxHeight = $derived(compact ? 120 : 200);
2442
@@ -29,15 +47,57 @@
2947
}
3048
}
3149
50+
function handleInput() {
51+
adjustTextarea();
52+
if (inputValue.startsWith("/") && !inputValue.includes(" ")) {
53+
const query = inputValue.slice(1).toLowerCase();
54+
filteredCommands = slashCommands.filter(c => c.name.startsWith(query));
55+
showSlashMenu = filteredCommands.length > 0;
56+
selectedCommandIdx = 0;
57+
} else {
58+
showSlashMenu = false;
59+
}
60+
}
61+
62+
function selectCommand(cmd: SlashCommand) {
63+
inputValue = cmd.prefix;
64+
showSlashMenu = false;
65+
textareaEl?.focus();
66+
}
67+
3268
function handleSend() {
3369
const text = inputValue.trim();
3470
if (!text || loading) return;
3571
inputValue = "";
72+
showSlashMenu = false;
3673
if (textareaEl) textareaEl.style.height = "auto";
3774
onSend(text);
3875
}
3976
4077
function handleKeydown(e: KeyboardEvent) {
78+
if (showSlashMenu) {
79+
if (e.key === "ArrowDown") {
80+
e.preventDefault();
81+
selectedCommandIdx = Math.min(selectedCommandIdx + 1, filteredCommands.length - 1);
82+
return;
83+
}
84+
if (e.key === "ArrowUp") {
85+
e.preventDefault();
86+
selectedCommandIdx = Math.max(selectedCommandIdx - 1, 0);
87+
return;
88+
}
89+
if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) {
90+
e.preventDefault();
91+
if (filteredCommands[selectedCommandIdx]) {
92+
selectCommand(filteredCommands[selectedCommandIdx]);
93+
}
94+
return;
95+
}
96+
if (e.key === "Escape") {
97+
showSlashMenu = false;
98+
return;
99+
}
100+
}
41101
if (e.key === "Enter" && !e.shiftKey) {
42102
e.preventDefault();
43103
handleSend();
@@ -50,6 +110,22 @@
50110
</script>
51111

52112
<div class="chat-input-wrap" class:compact>
113+
{#if showSlashMenu}
114+
<div class="slash-menu">
115+
{#each filteredCommands as cmd, idx}
116+
<button
117+
class="slash-item"
118+
class:active={idx === selectedCommandIdx}
119+
onclick={() => selectCommand(cmd)}
120+
>
121+
<Slash size={12} />
122+
<span class="slash-name">{cmd.name}</span>
123+
<span class="slash-desc">{cmd.description}</span>
124+
</button>
125+
{/each}
126+
</div>
127+
{/if}
128+
53129
<div class="input-container">
54130
<textarea
55131
bind:this={textareaEl}
@@ -58,7 +134,7 @@
58134
{placeholder}
59135
disabled={!ready || loading}
60136
rows="1"
61-
oninput={adjustTextarea}
137+
oninput={handleInput}
62138
onkeydown={handleKeydown}
63139
></textarea>
64140
<button
@@ -68,15 +144,15 @@
68144
aria-label="Send message"
69145
>
70146
{#if loading}
71-
<Loader2 class="h-{compact ? '4' : '5'} w-{compact ? '4' : '5'} animate-spin" />
147+
<Loader2 size={compact ? 16 : 20} class="animate-spin" />
72148
{:else}
73-
<Send class="h-{compact ? '4' : '5'} w-{compact ? '4' : '5'}" />
149+
<Send size={compact ? 16 : 20} />
74150
{/if}
75151
</button>
76152
</div>
77153
{#if !compact}
78154
<p class="input-hint">
79-
Codaro may produce inaccurate responses. Verify important information.
155+
Type <kbd>/</kbd> for commands. Shift+Enter for new line.
80156
</p>
81157
{/if}
82158
</div>
@@ -85,12 +161,64 @@
85161
.chat-input-wrap {
86162
flex-shrink: 0;
87163
padding: 12px 24px 16px;
164+
position: relative;
88165
}
89166
90167
.chat-input-wrap.compact {
91168
padding: 8px;
92169
}
93170
171+
.slash-menu {
172+
position: absolute;
173+
bottom: 100%;
174+
left: 24px;
175+
right: 24px;
176+
max-width: 768px;
177+
margin: 0 auto 4px;
178+
background: var(--background);
179+
border: 1px solid var(--border);
180+
border-radius: 10px;
181+
padding: 4px;
182+
box-shadow: 0 4px 12px color-mix(in srgb, var(--foreground) 10%, transparent);
183+
z-index: 10;
184+
}
185+
186+
.compact .slash-menu {
187+
left: 8px;
188+
right: 8px;
189+
border-radius: 8px;
190+
}
191+
192+
.slash-item {
193+
display: flex;
194+
align-items: center;
195+
gap: 8px;
196+
width: 100%;
197+
padding: 8px 10px;
198+
border: none;
199+
border-radius: 6px;
200+
background: transparent;
201+
color: var(--foreground);
202+
font-size: 0.85rem;
203+
cursor: pointer;
204+
text-align: left;
205+
transition: background 0.1s;
206+
}
207+
208+
.slash-item:hover,
209+
.slash-item.active {
210+
background: color-mix(in srgb, var(--accent) 10%, transparent);
211+
}
212+
213+
.slash-name {
214+
font-weight: 600;
215+
color: var(--accent);
216+
}
217+
218+
.slash-desc {
219+
color: color-mix(in srgb, var(--foreground) 50%, transparent);
220+
}
221+
94222
.input-container {
95223
max-width: 768px;
96224
margin: 0 auto;
@@ -181,4 +309,14 @@
181309
font-size: 0.72rem;
182310
color: color-mix(in srgb, var(--foreground) 30%, transparent);
183311
}
312+
313+
.input-hint kbd {
314+
display: inline-block;
315+
padding: 1px 4px;
316+
font-size: 0.7rem;
317+
font-family: inherit;
318+
border: 1px solid color-mix(in srgb, var(--border) 60%, transparent);
319+
border-radius: 3px;
320+
background: color-mix(in srgb, var(--foreground) 5%, transparent);
321+
}
184322
</style>

0 commit comments

Comments
 (0)