@gendive/chatllm 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +873 -0
- package/dist/react/index.d.mts +64 -2
- package/dist/react/index.d.ts +64 -2
- package/dist/react/index.js +1134 -122
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1127 -118
- package/dist/react/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
# @gendive/chatllm
|
|
2
|
+
|
|
3
|
+
Multi-LLM 채팅 라이브러리 - 모델 스위칭, 메모리, Function Calling, React UI 컴포넌트 지원
|
|
4
|
+
|
|
5
|
+
## 설치
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @gendive/chatllm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 패키지 구조
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
@gendive/chatllm
|
|
15
|
+
├── 코어 라이브러리 (import from '@gendive/chatllm')
|
|
16
|
+
│ ├── 채팅 세션 관리
|
|
17
|
+
│ ├── 멀티 프로바이더 지원 (OpenAI, Claude, Gemini, Ollama, DevDive)
|
|
18
|
+
│ ├── 메모리/컨텍스트 관리
|
|
19
|
+
│ └── Function Calling
|
|
20
|
+
│
|
|
21
|
+
└── React UI (import from '@gendive/chatllm/react')
|
|
22
|
+
├── ChatUI - 풀 기능 채팅 컴포넌트
|
|
23
|
+
├── useChatUI - Headless Hook
|
|
24
|
+
└── 개별 컴포넌트들
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 1. React UI 컴포넌트 (`@gendive/chatllm/react`)
|
|
30
|
+
|
|
31
|
+
### 1.1 ChatUI - 풀 기능 컴포넌트
|
|
32
|
+
|
|
33
|
+
가장 간단한 사용법. 모든 UI가 포함되어 있음.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
import { ChatUI } from '@gendive/chatllm/react'
|
|
37
|
+
|
|
38
|
+
const models = [
|
|
39
|
+
{ id: 'gpt-5', name: 'GPT-5', provider: 'devdive' },
|
|
40
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'devdive' },
|
|
41
|
+
{ id: 'claude-sonnet-4', name: 'Claude Sonnet 4', provider: 'devdive' },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
function App() {
|
|
45
|
+
return (
|
|
46
|
+
<div style={{ height: '100vh' }}>
|
|
47
|
+
<ChatUI
|
|
48
|
+
models={models}
|
|
49
|
+
apiKey="your-devdive-api-key"
|
|
50
|
+
apiEndpoint="/api/chat"
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 1.2 ChatUI Props
|
|
58
|
+
|
|
59
|
+
| Prop | Type | Default | 설명 |
|
|
60
|
+
|------|------|---------|------|
|
|
61
|
+
| `models` | `ModelConfig[]` | **필수** | 사용 가능한 모델 목록 |
|
|
62
|
+
| `actions` | `ActionItem[]` | 기본 4개 | 액션 버튼 (웹검색, 이미지생성 등) |
|
|
63
|
+
| `templates` | `PromptTemplate[]` | 기본 4개 | 시작 화면 프롬프트 템플릿 |
|
|
64
|
+
| `personalization` | `PersonalizationConfig` | - | 초기 개인화 설정 |
|
|
65
|
+
| `apiKey` | `string` | - | API 키 (DevDive 등 외부 프로바이더용) |
|
|
66
|
+
| `apiEndpoint` | `string` | `/api/chat` | API 엔드포인트 |
|
|
67
|
+
| `theme` | `ThemeConfig` | light | 테마 설정 (`{ mode: 'dark' }`) |
|
|
68
|
+
| `showSidebar` | `boolean` | `true` | 사이드바 표시 여부 |
|
|
69
|
+
| `storageKey` | `string` | `chatllm_sessions` | localStorage 키 |
|
|
70
|
+
| `contextCompressionThreshold` | `number` | `20` | 컨텍스트 압축 시작 메시지 수 |
|
|
71
|
+
| `onSendMessage` | `Function` | - | 커스텀 메시지 전송 핸들러 |
|
|
72
|
+
| `onSessionChange` | `Function` | - | 세션 변경 콜백 |
|
|
73
|
+
| `onError` | `Function` | - | 에러 핸들러 |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 2. 사용하는 측에서 구현해야 할 것들
|
|
78
|
+
|
|
79
|
+
### 2.1 API Route 구현 (Next.js 예시)
|
|
80
|
+
|
|
81
|
+
ChatUI는 `/api/chat`으로 요청을 보냅니다. SSE 스트리밍 응답을 반환해야 합니다.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// app/api/chat/route.ts
|
|
85
|
+
import { NextRequest } from 'next/server'
|
|
86
|
+
|
|
87
|
+
interface ChatRequest {
|
|
88
|
+
messages: { role: 'user' | 'assistant' | 'system'; content: string }[]
|
|
89
|
+
model: string
|
|
90
|
+
provider: 'ollama' | 'devdive' | 'openai'
|
|
91
|
+
apiKey?: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function POST(request: NextRequest) {
|
|
95
|
+
const { messages, model, provider, apiKey } = await request.json() as ChatRequest
|
|
96
|
+
|
|
97
|
+
// DevDive Gateway 예시
|
|
98
|
+
if (provider === 'devdive') {
|
|
99
|
+
const response = await fetch(`https://prd-gtw.devdive.ai/model/text-generation/v1/${model}`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
'X-API-KEY': apiKey || process.env.DEVDIVE_API_KEY!,
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
prompt: messages.filter(m => m.role !== 'system').map(m =>
|
|
107
|
+
`${m.role === 'user' ? 'User' : 'Assistant'}: ${m.content}`
|
|
108
|
+
).join('\n\n'),
|
|
109
|
+
system_prompt: messages.find(m => m.role === 'system')?.content || '',
|
|
110
|
+
}),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const data = await response.json()
|
|
114
|
+
|
|
115
|
+
// SSE 스트림으로 변환
|
|
116
|
+
const stream = new ReadableStream({
|
|
117
|
+
start(controller) {
|
|
118
|
+
const encoder = new TextEncoder()
|
|
119
|
+
const text = data.content.text
|
|
120
|
+
|
|
121
|
+
// 청크 단위로 전송 (스트리밍 효과)
|
|
122
|
+
let index = 0
|
|
123
|
+
const chunkSize = 10
|
|
124
|
+
|
|
125
|
+
const sendChunk = () => {
|
|
126
|
+
if (index < text.length) {
|
|
127
|
+
const chunk = text.slice(index, index + chunkSize)
|
|
128
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: chunk })}\n\n`))
|
|
129
|
+
index += chunkSize
|
|
130
|
+
setTimeout(sendChunk, 20)
|
|
131
|
+
} else {
|
|
132
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
|
|
133
|
+
controller.close()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sendChunk()
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
return new Response(stream, {
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Type': 'text/event-stream',
|
|
144
|
+
'Cache-Control': 'no-cache',
|
|
145
|
+
'Connection': 'keep-alive',
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 다른 프로바이더 처리...
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 2.2 설정 모달 구현
|
|
155
|
+
|
|
156
|
+
ChatUI는 헤더에 설정 버튼을 제공하지만, **설정 모달은 사용하는 측에서 구현해야 합니다**.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { ChatUI, useChatUI, PersonalizationConfig } from '@gendive/chatllm/react'
|
|
160
|
+
import { useState } from 'react'
|
|
161
|
+
|
|
162
|
+
function App() {
|
|
163
|
+
const [settingsOpen, setSettingsOpen] = useState(false)
|
|
164
|
+
const [personalization, setPersonalization] = useState<PersonalizationConfig>({
|
|
165
|
+
responseStyle: {
|
|
166
|
+
warmth: 'medium',
|
|
167
|
+
enthusiasm: 'medium',
|
|
168
|
+
emojiUsage: 'low',
|
|
169
|
+
formatting: 'default',
|
|
170
|
+
verbosity: 'balanced',
|
|
171
|
+
},
|
|
172
|
+
userProfile: {
|
|
173
|
+
nickname: '',
|
|
174
|
+
occupation: '',
|
|
175
|
+
additionalInfo: '',
|
|
176
|
+
},
|
|
177
|
+
useMemory: true,
|
|
178
|
+
language: 'auto',
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
<ChatUI
|
|
184
|
+
models={models}
|
|
185
|
+
personalization={personalization}
|
|
186
|
+
// 설정 버튼 클릭 시 모달 열기
|
|
187
|
+
// ChatUI 내부에서 openSettings 호출됨
|
|
188
|
+
/>
|
|
189
|
+
|
|
190
|
+
{/* 설정 모달 - 직접 구현 */}
|
|
191
|
+
{settingsOpen && (
|
|
192
|
+
<SettingsModal
|
|
193
|
+
personalization={personalization}
|
|
194
|
+
onSave={(newConfig) => {
|
|
195
|
+
setPersonalization(newConfig)
|
|
196
|
+
setSettingsOpen(false)
|
|
197
|
+
}}
|
|
198
|
+
onClose={() => setSettingsOpen(false)}
|
|
199
|
+
/>
|
|
200
|
+
)}
|
|
201
|
+
</>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 설정 모달 컴포넌트 예시
|
|
206
|
+
function SettingsModal({ personalization, onSave, onClose }) {
|
|
207
|
+
const [config, setConfig] = useState(personalization)
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="modal-overlay">
|
|
211
|
+
<div className="modal">
|
|
212
|
+
<h2>설정</h2>
|
|
213
|
+
|
|
214
|
+
{/* 사용자 프로필 */}
|
|
215
|
+
<section>
|
|
216
|
+
<h3>프로필</h3>
|
|
217
|
+
<input
|
|
218
|
+
placeholder="닉네임"
|
|
219
|
+
value={config.userProfile.nickname || ''}
|
|
220
|
+
onChange={(e) => setConfig({
|
|
221
|
+
...config,
|
|
222
|
+
userProfile: { ...config.userProfile, nickname: e.target.value }
|
|
223
|
+
})}
|
|
224
|
+
/>
|
|
225
|
+
<input
|
|
226
|
+
placeholder="직업/역할"
|
|
227
|
+
value={config.userProfile.occupation || ''}
|
|
228
|
+
onChange={(e) => setConfig({
|
|
229
|
+
...config,
|
|
230
|
+
userProfile: { ...config.userProfile, occupation: e.target.value }
|
|
231
|
+
})}
|
|
232
|
+
/>
|
|
233
|
+
</section>
|
|
234
|
+
|
|
235
|
+
{/* 응답 스타일 */}
|
|
236
|
+
<section>
|
|
237
|
+
<h3>응답 스타일</h3>
|
|
238
|
+
<label>
|
|
239
|
+
따뜻함
|
|
240
|
+
<select
|
|
241
|
+
value={config.responseStyle.warmth}
|
|
242
|
+
onChange={(e) => setConfig({
|
|
243
|
+
...config,
|
|
244
|
+
responseStyle: { ...config.responseStyle, warmth: e.target.value as any }
|
|
245
|
+
})}
|
|
246
|
+
>
|
|
247
|
+
<option value="low">사무적</option>
|
|
248
|
+
<option value="medium">보통</option>
|
|
249
|
+
<option value="high">친근함</option>
|
|
250
|
+
</select>
|
|
251
|
+
</label>
|
|
252
|
+
{/* 다른 스타일 옵션들... */}
|
|
253
|
+
</section>
|
|
254
|
+
|
|
255
|
+
<div className="modal-actions">
|
|
256
|
+
<button onClick={onClose}>취소</button>
|
|
257
|
+
<button onClick={() => onSave(config)}>저장</button>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 2.3 API 키 입력 UI
|
|
266
|
+
|
|
267
|
+
외부 프로바이더(DevDive, OpenAI 등) 사용 시 API 키 입력이 필요합니다.
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
function App() {
|
|
271
|
+
const [apiKey, setApiKey] = useState(() =>
|
|
272
|
+
localStorage.getItem('devdive_api_key') || ''
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// API 키가 없으면 입력 화면 표시
|
|
276
|
+
if (!apiKey) {
|
|
277
|
+
return (
|
|
278
|
+
<div className="api-key-setup">
|
|
279
|
+
<h2>API 키 설정</h2>
|
|
280
|
+
<p>DevDive API 키를 입력해주세요</p>
|
|
281
|
+
<input
|
|
282
|
+
type="password"
|
|
283
|
+
placeholder="API Key"
|
|
284
|
+
onChange={(e) => {
|
|
285
|
+
const key = e.target.value
|
|
286
|
+
setApiKey(key)
|
|
287
|
+
localStorage.setItem('devdive_api_key', key)
|
|
288
|
+
}}
|
|
289
|
+
/>
|
|
290
|
+
</div>
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<ChatUI
|
|
296
|
+
models={models}
|
|
297
|
+
apiKey={apiKey}
|
|
298
|
+
/>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 3. Headless Hook 사용 (커스텀 UI)
|
|
306
|
+
|
|
307
|
+
완전히 커스텀한 UI가 필요한 경우 `useChatUI` hook을 사용합니다.
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
import { useChatUI } from '@gendive/chatllm/react'
|
|
311
|
+
|
|
312
|
+
function CustomChatUI() {
|
|
313
|
+
const {
|
|
314
|
+
// State
|
|
315
|
+
messages,
|
|
316
|
+
input,
|
|
317
|
+
isLoading,
|
|
318
|
+
selectedModel,
|
|
319
|
+
sessions,
|
|
320
|
+
currentSession,
|
|
321
|
+
quotedText,
|
|
322
|
+
selectedAction,
|
|
323
|
+
|
|
324
|
+
// Actions
|
|
325
|
+
setInput,
|
|
326
|
+
sendMessage,
|
|
327
|
+
stopGeneration,
|
|
328
|
+
newSession,
|
|
329
|
+
selectSession,
|
|
330
|
+
deleteSession,
|
|
331
|
+
setModel,
|
|
332
|
+
setQuotedText,
|
|
333
|
+
setSelectedAction,
|
|
334
|
+
copyMessage,
|
|
335
|
+
regenerate,
|
|
336
|
+
updatePersonalization,
|
|
337
|
+
} = useChatUI({
|
|
338
|
+
models: [
|
|
339
|
+
{ id: 'gpt-5', name: 'GPT-5', provider: 'devdive' },
|
|
340
|
+
],
|
|
341
|
+
apiKey: 'your-api-key',
|
|
342
|
+
apiEndpoint: '/api/chat',
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div>
|
|
347
|
+
{/* 완전히 커스텀한 UI 구현 */}
|
|
348
|
+
<div className="messages">
|
|
349
|
+
{messages.map(msg => (
|
|
350
|
+
<div key={msg.id} className={msg.role}>
|
|
351
|
+
{msg.content}
|
|
352
|
+
</div>
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<form onSubmit={(e) => { e.preventDefault(); sendMessage(); }}>
|
|
357
|
+
<input
|
|
358
|
+
value={input}
|
|
359
|
+
onChange={(e) => setInput(e.target.value)}
|
|
360
|
+
placeholder="메시지 입력..."
|
|
361
|
+
/>
|
|
362
|
+
<button type="submit" disabled={isLoading}>
|
|
363
|
+
{isLoading ? '생성 중...' : '전송'}
|
|
364
|
+
</button>
|
|
365
|
+
</form>
|
|
366
|
+
</div>
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 4. 개별 컴포넌트 사용
|
|
374
|
+
|
|
375
|
+
ChatUI의 하위 컴포넌트들을 개별적으로 사용할 수 있습니다.
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
import {
|
|
379
|
+
ChatSidebar,
|
|
380
|
+
ChatHeader,
|
|
381
|
+
ChatInput,
|
|
382
|
+
MessageList,
|
|
383
|
+
EmptyState,
|
|
384
|
+
MemoryPanel,
|
|
385
|
+
} from '@gendive/chatllm/react'
|
|
386
|
+
|
|
387
|
+
// 예: 메시지 목록만 사용
|
|
388
|
+
<MessageList
|
|
389
|
+
messages={messages}
|
|
390
|
+
isLoading={isLoading}
|
|
391
|
+
onCopy={(content, id) => copyToClipboard(content)}
|
|
392
|
+
onEdit={(message) => startEdit(message)}
|
|
393
|
+
onRegenerate={(id) => regenerateMessage(id)}
|
|
394
|
+
onQuote={(text) => setQuotedText(text)}
|
|
395
|
+
copiedId={copiedId}
|
|
396
|
+
editingId={editingId}
|
|
397
|
+
/>
|
|
398
|
+
|
|
399
|
+
// 예: 입력창만 사용
|
|
400
|
+
<ChatInput
|
|
401
|
+
value={input}
|
|
402
|
+
onChange={setInput}
|
|
403
|
+
onSubmit={handleSubmit}
|
|
404
|
+
onStop={stopGeneration}
|
|
405
|
+
isLoading={isLoading}
|
|
406
|
+
quotedText={quotedText}
|
|
407
|
+
onClearQuote={() => setQuotedText(null)}
|
|
408
|
+
selectedAction={selectedAction}
|
|
409
|
+
onClearAction={() => setSelectedAction(null)}
|
|
410
|
+
actions={actions}
|
|
411
|
+
/>
|
|
412
|
+
|
|
413
|
+
// 예: 메모리 패널만 사용
|
|
414
|
+
<MemoryPanel
|
|
415
|
+
items={memoryItems}
|
|
416
|
+
contextSummary={contextSummary}
|
|
417
|
+
isOpen={isOpen}
|
|
418
|
+
onToggle={() => setIsOpen(!isOpen)}
|
|
419
|
+
onDelete={(id) => deleteMemoryItem(id)}
|
|
420
|
+
/>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## 5. 주요 기능 설명
|
|
426
|
+
|
|
427
|
+
### 5.1 액션 (Actions)
|
|
428
|
+
|
|
429
|
+
사용자가 입력창에서 선택할 수 있는 기능 블록입니다.
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
const customActions = [
|
|
433
|
+
{
|
|
434
|
+
id: 'webSearch',
|
|
435
|
+
label: '웹 검색',
|
|
436
|
+
icon: 'search', // 아이콘 종류: search, image, code, document
|
|
437
|
+
description: '웹에서 정보를 검색합니다',
|
|
438
|
+
systemPrompt: '웹 검색 결과를 기반으로 답변해주세요.',
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
id: 'translate',
|
|
442
|
+
label: '번역',
|
|
443
|
+
icon: 'document',
|
|
444
|
+
description: '텍스트를 번역합니다',
|
|
445
|
+
systemPrompt: '다음 텍스트를 한국어로 번역해주세요.',
|
|
446
|
+
},
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
<ChatUI models={models} actions={customActions} />
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**자동 감지**: 입력 내용에 따라 액션이 자동으로 선택됩니다.
|
|
453
|
+
- "검색해줘", "찾아줘" → 웹 검색
|
|
454
|
+
- "그림 그려줘", "이미지 만들어" → 이미지 생성
|
|
455
|
+
- "코드 분석", "버그 찾아" → 코드 분석
|
|
456
|
+
- "요약해줘", "정리해" → 요약
|
|
457
|
+
|
|
458
|
+
### 5.2 컨텍스트 압축
|
|
459
|
+
|
|
460
|
+
대화가 길어지면 자동으로 이전 대화를 요약하여 컨텍스트를 관리합니다.
|
|
461
|
+
|
|
462
|
+
```tsx
|
|
463
|
+
<ChatUI
|
|
464
|
+
models={models}
|
|
465
|
+
contextCompressionThreshold={20} // 20개 메시지 초과 시 압축
|
|
466
|
+
keepRecentMessages={6} // 최근 6개 메시지는 유지
|
|
467
|
+
/>
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### 5.3 텍스트 선택 인용
|
|
471
|
+
|
|
472
|
+
메시지 내 텍스트를 드래그하면 "인용하기" 버튼이 나타납니다.
|
|
473
|
+
클릭하면 입력창에 인용 칩이 추가됩니다.
|
|
474
|
+
|
|
475
|
+
### 5.4 메모리 패널
|
|
476
|
+
|
|
477
|
+
우측 하단의 AI 아이콘을 클릭하면 메모리 패널이 열립니다.
|
|
478
|
+
- 대화 컨텍스트 요약
|
|
479
|
+
- 사용자 선호 정보
|
|
480
|
+
- 학습된 정보
|
|
481
|
+
|
|
482
|
+
### 5.5 다크 모드
|
|
483
|
+
|
|
484
|
+
```tsx
|
|
485
|
+
<ChatUI
|
|
486
|
+
models={models}
|
|
487
|
+
theme={{ mode: 'dark' }}
|
|
488
|
+
/>
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
---
|
|
492
|
+
|
|
493
|
+
## 6. 타입 정의
|
|
494
|
+
|
|
495
|
+
### ModelConfig
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
interface ModelConfig {
|
|
499
|
+
id: string // 모델 ID (API 호출 시 사용)
|
|
500
|
+
name: string // 표시 이름
|
|
501
|
+
provider: 'ollama' | 'devdive' | 'openai' | 'anthropic' | 'gemini'
|
|
502
|
+
description?: string // 설명 (선택)
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### PersonalizationConfig
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
interface PersonalizationConfig {
|
|
510
|
+
responseStyle: {
|
|
511
|
+
warmth: 'low' | 'medium' | 'high' // 따뜻함
|
|
512
|
+
enthusiasm: 'low' | 'medium' | 'high' // 열정
|
|
513
|
+
emojiUsage: 'low' | 'medium' | 'high' // 이모지 사용
|
|
514
|
+
formatting: 'minimal' | 'default' | 'rich' // 포맷팅
|
|
515
|
+
verbosity: 'concise' | 'balanced' | 'detailed' // 응답 길이
|
|
516
|
+
}
|
|
517
|
+
userProfile: {
|
|
518
|
+
nickname?: string // 사용자 닉네임
|
|
519
|
+
occupation?: string // 직업/역할
|
|
520
|
+
additionalInfo?: string // 추가 정보
|
|
521
|
+
preferredLanguage?: string
|
|
522
|
+
}
|
|
523
|
+
useMemory: boolean // 메모리 사용 여부
|
|
524
|
+
language: string // 응답 언어 ('auto' | 'ko' | 'en' 등)
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### ActionItem
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
interface ActionItem {
|
|
532
|
+
id: string
|
|
533
|
+
label: string // UI에 표시할 레이블
|
|
534
|
+
icon: string // 아이콘 종류
|
|
535
|
+
description: string // 설명
|
|
536
|
+
systemPrompt?: string // AI에게 전달할 시스템 프롬프트
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## 7. CSS 커스터마이징
|
|
543
|
+
|
|
544
|
+
CSS 변수를 오버라이드하여 스타일을 커스터마이즈할 수 있습니다.
|
|
545
|
+
|
|
546
|
+
```css
|
|
547
|
+
.chatllm-root {
|
|
548
|
+
--chatllm-primary: #3b82f6;
|
|
549
|
+
--chatllm-primary-hover: #2563eb;
|
|
550
|
+
--chatllm-primary-light: #dbeafe;
|
|
551
|
+
--chatllm-bg: #ffffff;
|
|
552
|
+
--chatllm-bg-secondary: #f9fafb;
|
|
553
|
+
--chatllm-text: #1f2937;
|
|
554
|
+
--chatllm-text-muted: #6b7280;
|
|
555
|
+
--chatllm-border: #e5e7eb;
|
|
556
|
+
/* ... */
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
---
|
|
561
|
+
|
|
562
|
+
## 8. Demo 기능 가이드
|
|
563
|
+
|
|
564
|
+
demo 폴더에 포함된 Next.js 앱에서 제공하는 전체 기능 목록입니다.
|
|
565
|
+
|
|
566
|
+
### 8.1 세션 관리 (사이드바)
|
|
567
|
+
|
|
568
|
+
```
|
|
569
|
+
┌─────────────────────────────────────────┐
|
|
570
|
+
│ ⚡ devdive-chatLLM [새 대화] [☰] │
|
|
571
|
+
├─────────────────────────────────────────┤
|
|
572
|
+
│ 🔍 대화 검색... │
|
|
573
|
+
├─────────────────────────────────────────┤
|
|
574
|
+
│ 📅 오늘 │
|
|
575
|
+
│ ├─ 코드 리뷰 요청... [🗑] │
|
|
576
|
+
│ └─ API 설계 질문... [🗑] │
|
|
577
|
+
│ │
|
|
578
|
+
│ 📅 어제 │
|
|
579
|
+
│ └─ 블로그 글 작성... [🗑] │
|
|
580
|
+
└─────────────────────────────────────────┘
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
- **새 대화 버튼**: 새 세션 생성
|
|
584
|
+
- **세션 목록**: 날짜별 그룹핑, 클릭으로 전환
|
|
585
|
+
- **세션 삭제**: 휴지통 아이콘 클릭
|
|
586
|
+
- **자동 제목 생성**: 첫 번째 사용자 메시지로 제목 생성
|
|
587
|
+
- **localStorage 저장**: 브라우저 새로고침 후에도 유지
|
|
588
|
+
|
|
589
|
+
### 8.2 멀티 모델 지원
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
// 헤더의 모델 선택 드롭다운
|
|
593
|
+
const AVAILABLE_MODELS = [
|
|
594
|
+
// Ollama (로컬) - localhost:11434
|
|
595
|
+
{ id: 'qwen3:4b', name: 'Qwen3 4B', provider: 'ollama' },
|
|
596
|
+
{ id: 'llama3.1:8b', name: 'Llama 3.1 8B', provider: 'ollama' },
|
|
597
|
+
|
|
598
|
+
// DevDive Gateway (외부) - API 키 필요
|
|
599
|
+
{ id: 'gpt-5', name: 'GPT-5', provider: 'devdive' },
|
|
600
|
+
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini', provider: 'devdive' },
|
|
601
|
+
{ id: 'claude-sonnet-4', name: 'Claude Sonnet 4', provider: 'devdive' },
|
|
602
|
+
]
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
- **Ollama**: 로컬에서 실행되는 모델 (API 키 불필요)
|
|
606
|
+
- **DevDive Gateway**: 외부 API (API 키 필요, 설정에서 입력)
|
|
607
|
+
|
|
608
|
+
### 8.3 액션 기능 (Actions)
|
|
609
|
+
|
|
610
|
+
입력창 좌측의 `+` 버튼 또는 자동 감지로 액션을 선택합니다.
|
|
611
|
+
|
|
612
|
+
```
|
|
613
|
+
┌──────────────────────────────────────┐
|
|
614
|
+
│ [+ 액션] 메시지를 입력하세요... │
|
|
615
|
+
│ │
|
|
616
|
+
│ ┌─────────────────────────────────┐ │
|
|
617
|
+
│ │ 🔍 웹 검색 │ │
|
|
618
|
+
│ │ 웹에서 정보를 검색합니다 │ │
|
|
619
|
+
│ ├─────────────────────────────────┤ │
|
|
620
|
+
│ │ 🖼 이미지 생성 │ │
|
|
621
|
+
│ │ 프롬프트로 이미지를 생성합니다│ │
|
|
622
|
+
│ ├─────────────────────────────────┤ │
|
|
623
|
+
│ │ 💻 코드 분석 │ │
|
|
624
|
+
│ │ 코드를 분석하고 설명합니다 │ │
|
|
625
|
+
│ ├─────────────────────────────────┤ │
|
|
626
|
+
│ │ 📄 요약 │ │
|
|
627
|
+
│ │ 긴 텍스트를 요약합니다 │ │
|
|
628
|
+
│ └─────────────────────────────────┘ │
|
|
629
|
+
└──────────────────────────────────────┘
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**자동 감지 키워드:**
|
|
633
|
+
| 액션 | 감지 키워드 |
|
|
634
|
+
|------|-------------|
|
|
635
|
+
| 웹 검색 | "검색", "찾아", "search", "구글", "최신", "알려" |
|
|
636
|
+
| 이미지 생성 | "이미지", "그림", "그려", "image", "draw" |
|
|
637
|
+
| 코드 분석 | "코드", "분석", "리뷰", "디버그", "에러" |
|
|
638
|
+
| 요약 | "요약", "summarize", "줄여", "핵심", "정리" |
|
|
639
|
+
|
|
640
|
+
선택된 액션은 입력창에 칩으로 표시되고, `systemPrompt`가 AI에게 전달됩니다.
|
|
641
|
+
|
|
642
|
+
### 8.4 텍스트 선택 인용
|
|
643
|
+
|
|
644
|
+
메시지 내 텍스트를 드래그하면 인용 팝업이 나타납니다.
|
|
645
|
+
|
|
646
|
+
```
|
|
647
|
+
AI: React는 컴포넌트 기반의 UI 라이브러리입니다.
|
|
648
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^ (드래그)
|
|
649
|
+
┌─────────────┐
|
|
650
|
+
│ 📝 인용하기 │ ← 클릭
|
|
651
|
+
└─────────────┘
|
|
652
|
+
|
|
653
|
+
┌──────────────────────────────────────┐
|
|
654
|
+
│ ┌────────────────────────────────┐ │
|
|
655
|
+
│ │ 📌 "컴포넌트 기반의 UI 라이브러리" │ [✕] │
|
|
656
|
+
│ └────────────────────────────────┘ │
|
|
657
|
+
│ │
|
|
658
|
+
│ 이것에 대해 더 자세히 설명해줘... │
|
|
659
|
+
└──────────────────────────────────────┘
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
인용 칩이 입력창 위에 표시되고, 메시지와 함께 전송됩니다.
|
|
663
|
+
|
|
664
|
+
### 8.5 개인화 설정 (Settings)
|
|
665
|
+
|
|
666
|
+
헤더의 설정 버튼 클릭 시 모달이 열립니다.
|
|
667
|
+
|
|
668
|
+
**일반 탭:**
|
|
669
|
+
- 테마: 라이트/다크
|
|
670
|
+
- 언어: 자동/한국어/영어/일본어
|
|
671
|
+
|
|
672
|
+
**개인화 탭:**
|
|
673
|
+
```
|
|
674
|
+
┌────────────────────────────────────────┐
|
|
675
|
+
│ 📝 사용자 프로필 │
|
|
676
|
+
├────────────────────────────────────────┤
|
|
677
|
+
│ 닉네임: [민수 ] │
|
|
678
|
+
│ 직업: [개발자 ] │
|
|
679
|
+
│ 추가정보: [TypeScript 선호] │
|
|
680
|
+
├────────────────────────────────────────┤
|
|
681
|
+
│ 🎨 응답 스타일 │
|
|
682
|
+
├────────────────────────────────────────┤
|
|
683
|
+
│ 따뜻함: [낮음 | 보통 | 높음] │
|
|
684
|
+
│ 열정: [낮음 | 보통 | 높음] │
|
|
685
|
+
│ 이모지: [적게 | 보통 | 많이] │
|
|
686
|
+
│ 상세함: [간결 | 균형 | 상세] │
|
|
687
|
+
└────────────────────────────────────────┘
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
설정된 정보는 시스템 프롬프트로 자동 변환됩니다:
|
|
691
|
+
```
|
|
692
|
+
사용자의 이름/닉네임: 민수
|
|
693
|
+
사용자의 직업: 개발자
|
|
694
|
+
사용자 추가 정보: TypeScript 선호
|
|
695
|
+
응답 스타일: 따뜻하고 친근하게, 핵심만 간결하게 응답해주세요.
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
**데이터 관리 탭:**
|
|
699
|
+
- 대화 기록 내보내기 (JSON)
|
|
700
|
+
- 전체 데이터 삭제
|
|
701
|
+
|
|
702
|
+
### 8.6 컨텍스트 압축
|
|
703
|
+
|
|
704
|
+
대화가 길어지면 자동으로 이전 대화를 요약합니다.
|
|
705
|
+
|
|
706
|
+
```
|
|
707
|
+
┌─────────────────────────────────────────┐
|
|
708
|
+
│ 📋 컨텍스트 요약됨 │
|
|
709
|
+
│ "사용자는 React 프로젝트에서 상태 관리를 │
|
|
710
|
+
│ Redux에서 Zustand로 마이그레이션 중..." │
|
|
711
|
+
└─────────────────────────────────────────┘
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
- **임계값**: 20개 메시지 초과 시 압축
|
|
715
|
+
- **유지 메시지**: 최근 6개 메시지는 원본 유지
|
|
716
|
+
- **압축 저장**: `contextSummary` 필드에 저장
|
|
717
|
+
|
|
718
|
+
### 8.7 메시지 액션
|
|
719
|
+
|
|
720
|
+
각 메시지에 호버하면 액션 버튼이 나타납니다.
|
|
721
|
+
|
|
722
|
+
**사용자 메시지:**
|
|
723
|
+
```
|
|
724
|
+
┌────────────────────────────────────┐
|
|
725
|
+
│ 나: React 상태관리 방법 알려줘 │ [📋 복사] [✏️ 편집]
|
|
726
|
+
└────────────────────────────────────┘
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
**AI 응답 메시지:**
|
|
730
|
+
```
|
|
731
|
+
┌────────────────────────────────────┐
|
|
732
|
+
│ AI: Redux, Zustand, Jotai 등이... │ [📋 복사] [🔄 재생성] [🤖 다른 모델]
|
|
733
|
+
└────────────────────────────────────┘
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
- **복사**: 클립보드에 복사
|
|
737
|
+
- **편집**: 사용자 메시지 수정 후 재전송
|
|
738
|
+
- **재생성**: 동일 질문에 새 응답 생성
|
|
739
|
+
- **다른 모델로 질문**: 다른 LLM에게 동일 질문
|
|
740
|
+
|
|
741
|
+
### 8.8 다른 모델로 비교하기
|
|
742
|
+
|
|
743
|
+
"다른 모델로 질문" 클릭 시 모델 선택 드롭다운이 나타납니다.
|
|
744
|
+
|
|
745
|
+
```
|
|
746
|
+
┌────────────────────────────────────────┐
|
|
747
|
+
│ AI (GPT-4o Mini): Redux는... │
|
|
748
|
+
│ │
|
|
749
|
+
│ ┌─ 다른 모델 비교 ─────────────────────┐│
|
|
750
|
+
│ │ GPT-5 응답: ││
|
|
751
|
+
│ │ "상태 관리 방법으로는..." ││
|
|
752
|
+
│ ├─────────────────────────────────────┤│
|
|
753
|
+
│ │ Claude Sonnet 4 응답: ││
|
|
754
|
+
│ │ "React에서 상태를 관리할 때는..." ││
|
|
755
|
+
│ └─────────────────────────────────────┘│
|
|
756
|
+
│ │
|
|
757
|
+
│ [◀ 이전] [2/3] [다음 ▶] │
|
|
758
|
+
└────────────────────────────────────────┘
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
`alternatives` 배열에 다른 모델의 응답이 저장됩니다.
|
|
762
|
+
|
|
763
|
+
### 8.9 시작 화면 템플릿
|
|
764
|
+
|
|
765
|
+
새 세션에서 빈 대화창에 추천 템플릿이 표시됩니다.
|
|
766
|
+
|
|
767
|
+
```
|
|
768
|
+
┌───────────────────────────────────────────────┐
|
|
769
|
+
│ 무엇을 도와드릴까요? │
|
|
770
|
+
│ │
|
|
771
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
|
772
|
+
│ │ ✍️ AI 글쓰기 │ │ 💻 코드 리뷰 │ │
|
|
773
|
+
│ │ 블로그, 이메일│ │ 분석 및 개선 │ │
|
|
774
|
+
│ └─────────────┘ └─────────────┘ │
|
|
775
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
|
776
|
+
│ │ 🌐 번역 도움 │ │ 📋 요약 정리 │ │
|
|
777
|
+
│ │ 자연스러운 │ │ 핵심만 요약 │ │
|
|
778
|
+
│ └─────────────┘ └─────────────┘ │
|
|
779
|
+
└───────────────────────────────────────────────┘
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
클릭하면 해당 템플릿의 프롬프트가 입력창에 자동 입력됩니다.
|
|
783
|
+
|
|
784
|
+
### 8.10 API 키 관리
|
|
785
|
+
|
|
786
|
+
DevDive 프로바이더 사용 시 API 키가 필요합니다.
|
|
787
|
+
|
|
788
|
+
```
|
|
789
|
+
설정 > 일반 > API 키
|
|
790
|
+
┌─────────────────────────────────────┐
|
|
791
|
+
│ DevDive API Key │
|
|
792
|
+
│ [••••••••••••••••••••••••] [저장] │
|
|
793
|
+
│ │
|
|
794
|
+
│ ⚠️ API 키는 브라우저에 저장됩니다 │
|
|
795
|
+
└─────────────────────────────────────┘
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
- `localStorage`에 암호화 없이 저장
|
|
799
|
+
- 프로덕션에서는 서버 사이드 프록시 권장
|
|
800
|
+
|
|
801
|
+
---
|
|
802
|
+
|
|
803
|
+
## 9. API Route 상세 (demo/api/chat)
|
|
804
|
+
|
|
805
|
+
demo에서 사용하는 API route 구조입니다.
|
|
806
|
+
|
|
807
|
+
### 요청 형식
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
interface ChatRequest {
|
|
811
|
+
messages: {
|
|
812
|
+
role: 'user' | 'assistant' | 'system'
|
|
813
|
+
content: string
|
|
814
|
+
}[]
|
|
815
|
+
model: string // 'qwen3:4b', 'gpt-5' 등
|
|
816
|
+
provider: 'ollama' | 'devdive'
|
|
817
|
+
apiKey?: string // DevDive용 API 키
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### 응답 형식 (SSE)
|
|
822
|
+
|
|
823
|
+
```
|
|
824
|
+
data: {"content": "안녕"}
|
|
825
|
+
|
|
826
|
+
data: {"content": "하세요"}
|
|
827
|
+
|
|
828
|
+
data: {"content": "!"}
|
|
829
|
+
|
|
830
|
+
data: [DONE]
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
### Ollama 호출 예시
|
|
834
|
+
|
|
835
|
+
```typescript
|
|
836
|
+
// localhost:11434/api/chat로 스트리밍 요청
|
|
837
|
+
const response = await fetch('http://localhost:11434/api/chat', {
|
|
838
|
+
method: 'POST',
|
|
839
|
+
body: JSON.stringify({
|
|
840
|
+
model: 'qwen3:4b',
|
|
841
|
+
messages: [...],
|
|
842
|
+
stream: true,
|
|
843
|
+
}),
|
|
844
|
+
})
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### DevDive Gateway 호출 예시
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
// DevDive Gateway API
|
|
851
|
+
const response = await fetch(
|
|
852
|
+
`https://prd-gtw.devdive.ai/model/text-generation/v1/${model}`,
|
|
853
|
+
{
|
|
854
|
+
method: 'POST',
|
|
855
|
+
headers: {
|
|
856
|
+
'Content-Type': 'application/json',
|
|
857
|
+
'X-API-KEY': apiKey,
|
|
858
|
+
},
|
|
859
|
+
body: JSON.stringify({
|
|
860
|
+
prompt: '...',
|
|
861
|
+
system_prompt: '...',
|
|
862
|
+
max_tokens: 2000,
|
|
863
|
+
temperature: 0.7,
|
|
864
|
+
}),
|
|
865
|
+
}
|
|
866
|
+
)
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
## 라이선스
|
|
872
|
+
|
|
873
|
+
MIT
|