Skip to content

Commit 7d59402

Browse files
committed
process selected file
1 parent f4af3f3 commit 7d59402

File tree

9 files changed

+181
-52
lines changed

9 files changed

+181
-52
lines changed

common/chat.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ std::vector<common_chat_msg> common_chat_msgs_parse_oaicompat(const json & messa
125125
msgs.push_back(msg);
126126
}
127127
} catch (const std::exception & e) {
128-
throw std::runtime_error("Failed to parse messages: " + std::string(e.what()) + "; messages = " + messages.dump(2));
128+
// @ngxson : disable otherwise it's bloating the API response
129+
// printf("%s\n", std::string("; messages = ") + messages.dump(2));
130+
throw std::runtime_error("Failed to parse messages: " + std::string(e.what()));
129131
}
130132

131133
return msgs;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { DocumentTextIcon, XMarkIcon } from '@heroicons/react/24/outline';
2+
import { MessageExtra } from '../utils/types';
3+
import { useState } from 'react';
4+
import { classNames } from '../utils/misc';
5+
6+
export default function ChatInputExtraContextItem({
7+
items,
8+
removeItem,
9+
clickToShow,
10+
}: {
11+
items?: MessageExtra[];
12+
removeItem?: (index: number) => void;
13+
clickToShow?: boolean;
14+
}) {
15+
const [show, setShow] = useState(-1);
16+
const showingItem = show >= 0 ? items?.[show] : undefined;
17+
18+
if (!items) return null;
19+
20+
return (
21+
<div className="flex flex-row gap-4 overflow-x-auto py-2 px-1 mb-1">
22+
{items.map((item, i) => (
23+
<div
24+
className="indicator"
25+
key={i}
26+
onClick={() => clickToShow && setShow(i)}
27+
>
28+
{removeItem && (
29+
<div className="indicator-item indicator-top">
30+
<button
31+
className="btn btn-neutral btn-sm w-4 h-4 p-0 rounded-full"
32+
onClick={() => removeItem(i)}
33+
>
34+
<XMarkIcon className="h-3 w-3" />
35+
</button>
36+
</div>
37+
)}
38+
39+
<div
40+
className={classNames({
41+
'flex flex-row rounded-md shadow-sm items-center m-0 p-0': true,
42+
'cursor-pointer hover:shadow-md': !!clickToShow,
43+
})}
44+
>
45+
{item.type === 'imageFile' ? (
46+
<>
47+
<img
48+
src={item.base64Url}
49+
alt={item.name}
50+
className="w-14 h-14 object-cover rounded-md"
51+
/>
52+
</>
53+
) : (
54+
<>
55+
<div className="w-14 h-14 flex items-center justify-center">
56+
<DocumentTextIcon className="h-8 w-14 text-base-content/50" />
57+
</div>
58+
59+
<div className="text-xs pr-4">
60+
<b>{item.name}</b>
61+
</div>
62+
</>
63+
)}
64+
</div>
65+
</div>
66+
))}
67+
68+
{showingItem && (
69+
<dialog className="modal modal-open">
70+
<div className="modal-box">
71+
<div className="flex justify-between items-center mb-4">
72+
<b>{showingItem.name}</b>
73+
<button className="btn btn-ghost btn-sm">
74+
<XMarkIcon className="h-5 w-5" onClick={() => setShow(-1)} />
75+
</button>
76+
</div>
77+
{showingItem.type === 'imageFile' ? (
78+
<img src={showingItem.base64Url} alt={showingItem.name} />
79+
) : (
80+
<div className="overflow-x-auto">
81+
<pre className="whitespace-pre-wrap break-words text-sm">
82+
{showingItem.content}
83+
</pre>
84+
</div>
85+
)}
86+
</div>
87+
<div className="modal-backdrop" onClick={() => setShow(-1)}></div>
88+
</dialog>
89+
)}
90+
</div>
91+
);
92+
}

tools/server/webui/src/components/ChatMessage.tsx

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Message, PendingMessage } from '../utils/types';
44
import { classNames } from '../utils/misc';
55
import MarkdownDisplay, { CopyButton } from './MarkdownDisplay';
66
import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
7+
import ChatInputExtraContextItem from './ChatInputExtraContextItem';
78

89
interface SplitMessage {
910
content: PendingMessage['content'];
@@ -85,6 +86,10 @@ export default function ChatMessage({
8586
'chat-end': msg.role === 'user',
8687
})}
8788
>
89+
{msg.extra && msg.extra.length > 0 && (
90+
<ChatInputExtraContextItem items={msg.extra} clickToShow />
91+
)}
92+
8893
<div
8994
className={classNames({
9095
'chat-bubble markdown': true,
@@ -160,34 +165,6 @@ export default function ChatMessage({
160165
</details>
161166
)}
162167

163-
{msg.extra && msg.extra.length > 0 && (
164-
<details
165-
className={classNames({
166-
'collapse collapse-arrow mb-4 bg-base-200': true,
167-
'bg-opacity-10': msg.role !== 'assistant',
168-
})}
169-
>
170-
<summary className="collapse-title">
171-
Extra content
172-
</summary>
173-
<div className="collapse-content">
174-
{msg.extra.map(
175-
(extra, i) =>
176-
extra.type === 'textFile' ? (
177-
<div key={extra.name}>
178-
<b>{extra.name}</b>
179-
<pre>{extra.content}</pre>
180-
</div>
181-
) : extra.type === 'context' ? (
182-
<div key={i}>
183-
<pre>{extra.content}</pre>
184-
</div>
185-
) : null // TODO: support other extra types
186-
)}
187-
</div>
188-
</details>
189-
)}
190-
191168
<MarkdownDisplay
192169
content={content}
193170
isGenerating={isPending}

tools/server/webui/src/components/ChatScreen.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useMemo, useState } from 'react';
22
import { CallbackGeneratedChunk, useAppContext } from '../utils/app.context';
33
import ChatMessage from './ChatMessage';
4-
import { CanvasType, Message, MessageExtraContext, PendingMessage } from '../utils/types';
4+
import { CanvasType, Message, PendingMessage } from '../utils/types';
55
import { classNames, cleanCurrentUrl, throttle } from '../utils/misc';
66
import CanvasPyInterpreter from './CanvasPyInterpreter';
77
import StorageUtils from '../utils/storage';
@@ -12,12 +12,15 @@ import {
1212
StopIcon,
1313
PaperClipIcon,
1414
DocumentTextIcon,
15+
XMarkIcon,
1516
} from '@heroicons/react/24/solid';
1617
import {
1718
ChatExtraContextApi,
1819
useChatExtraContext,
1920
} from './useChatExtraContext.tsx';
2021
import Dropzone from 'react-dropzone';
22+
import toast from 'react-hot-toast';
23+
import ChatInputExtraContextItem from './ChatInputExtraContextItem.tsx';
2124

2225
/**
2326
* A message display is a message node with additional information for rendering.
@@ -143,8 +146,10 @@ export default function ChatScreen() {
143146

144147
const sendNewMessage = async () => {
145148
const lastInpMsg = textarea.value();
146-
if (lastInpMsg.trim().length === 0 || isGenerating(currConvId ?? ''))
149+
if (lastInpMsg.trim().length === 0 || isGenerating(currConvId ?? '')) {
150+
toast.error('Please enter a message');
147151
return;
152+
}
148153
textarea.setValue('');
149154
scrollToBottom(false);
150155
setCurrNodeId(-1);
@@ -312,7 +317,16 @@ function ChatInput({
312317
multiple={true}
313318
>
314319
{({ getRootProps, getInputProps }) => (
315-
<div className="flex rounded-xl border-1 border-base-content/30 p-3 w-full" {...getRootProps()}>
320+
<div
321+
className="flex flex-col rounded-xl border-1 border-base-content/30 p-3 w-full"
322+
{...getRootProps()}
323+
>
324+
<ChatInputExtraContextItem
325+
items={extraContext.items}
326+
removeItem={extraContext.removeItem}
327+
/>
328+
329+
<div className="flex flex-row w-full">
316330
<textarea
317331
// Default (mobile): Enable vertical resize, overflow auto for scrolling if needed
318332
// Large screens (lg:): Disable manual resize, apply max-height for autosize limit
@@ -367,15 +381,9 @@ function ChatInput({
367381
)}
368382
</div>
369383
</div>
384+
</div>
370385
)}
371386
</Dropzone>
372387
</div>
373388
);
374389
}
375-
376-
function ChatInputExtraContextItem({}: {
377-
idx: number,
378-
item: MessageExtraContext,
379-
}) {
380-
381-
}

tools/server/webui/src/components/Sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export default function Sidebar() {
5656
<div
5757
className={classNames({
5858
'btn btn-ghost justify-start': true,
59-
'btn-active': !currConv,
59+
'btn-soft': !currConv,
6060
})}
6161
onClick={() => navigate('/')}
6262
>
@@ -67,7 +67,7 @@ export default function Sidebar() {
6767
key={conv.id}
6868
className={classNames({
6969
'btn btn-ghost justify-start font-normal': true,
70-
'btn-active': conv.id === currConv?.id,
70+
'btn-soft': conv.id === currConv?.id,
7171
})}
7272
onClick={() => navigate(`/chat/${conv.id}`)}
7373
dir="auto"

tools/server/webui/src/utils/app.context.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from './misc';
1616
import { BASE_URL, CONFIG_DEFAULT, isDev } from '../Config';
1717
import { matchPath, useLocation, useNavigate } from 'react-router';
18+
import toast from 'react-hot-toast';
1819

1920
interface AppContextValue {
2021
// conversations and messages
@@ -260,7 +261,7 @@ export const AppContextProvider = ({
260261
} else {
261262
console.error(err);
262263
// eslint-disable-next-line @typescript-eslint/no-explicit-any
263-
alert((err as any)?.message ?? 'Unknown error');
264+
toast.error((err as any)?.message ?? 'Unknown error');
264265
throw err; // rethrow
265266
}
266267
}

tools/server/webui/src/utils/llama-vscode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export const useVSCodeContext = (
3131
extraContext.addItems([
3232
{
3333
type: 'context',
34+
name: 'Extra context',
3435
content: data.context,
3536
},
3637
]);

tools/server/webui/src/utils/misc.ts

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @ts-expect-error this package does not have typing
22
import TextLineStream from 'textlinestream';
3-
import { APIMessage, Message } from './types';
3+
import { APIMessage, APIMessageContentPart, Message } from './types';
44

55
// ponyfill for missing ReadableStream asyncIterator on Safari
66
import { asyncIterator } from '@sec-ant/readable-stream/ponyfill/asyncIterator';
@@ -57,19 +57,44 @@ export const copyStr = (textToCopy: string) => {
5757
*/
5858
export function normalizeMsgsForAPI(messages: Readonly<Message[]>) {
5959
return messages.map((msg) => {
60-
let newContent = '';
60+
if (msg.role !== 'user' || !msg.extra) {
61+
return {
62+
role: msg.role,
63+
content: msg.content,
64+
} as APIMessage;
65+
}
66+
67+
const contentArr: APIMessageContentPart[] = [
68+
{
69+
type: 'text',
70+
text: msg.content,
71+
},
72+
];
6173

6274
for (const extra of msg.extra ?? []) {
6375
if (extra.type === 'context') {
64-
newContent += `${extra.content}\n\n`;
76+
contentArr.push({
77+
type: 'text',
78+
text: extra.content,
79+
});
80+
} else if (extra.type === 'textFile') {
81+
contentArr.push({
82+
type: 'text',
83+
text: `File: ${extra.name}\nContent:\n\n${extra.content}`,
84+
});
85+
} else if (extra.type === 'imageFile') {
86+
contentArr.push({
87+
type: 'image_url',
88+
image_url: { url: extra.base64Url },
89+
});
90+
} else {
91+
throw new Error('Unknown extra type');
6592
}
6693
}
6794

68-
newContent += msg.content;
69-
7095
return {
7196
role: msg.role,
72-
content: newContent,
97+
content: contentArr,
7398
};
7499
}) as APIMessage[];
75100
}
@@ -78,13 +103,19 @@ export function normalizeMsgsForAPI(messages: Readonly<Message[]>) {
78103
* recommended for DeepsSeek-R1, filter out content between <think> and </think> tags
79104
*/
80105
export function filterThoughtFromMsgs(messages: APIMessage[]) {
106+
console.debug({ messages });
81107
return messages.map((msg) => {
108+
if (msg.role !== 'assistant') {
109+
return msg;
110+
}
111+
// assistant message is always a string
112+
const contentStr = msg.content as string;
82113
return {
83114
role: msg.role,
84115
content:
85116
msg.role === 'assistant'
86-
? msg.content.split('</think>').at(-1)!.trim()
87-
: msg.content,
117+
? contentStr.split('</think>').at(-1)!.trim()
118+
: contentStr,
88119
} as APIMessage;
89120
});
90121
}

tools/server/webui/src/utils/types.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ export interface Message {
4848
children: Message['id'][];
4949
}
5050

51-
export type MessageExtra = MessageExtraTextFile | MessageExtraImageFile | MessageExtraContext;
51+
export type MessageExtra =
52+
| MessageExtraTextFile
53+
| MessageExtraImageFile
54+
| MessageExtraContext;
5255

5356
export interface MessageExtraTextFile {
5457
type: 'textFile';
@@ -64,10 +67,24 @@ export interface MessageExtraImageFile {
6467

6568
export interface MessageExtraContext {
6669
type: 'context';
70+
name: string;
6771
content: string;
6872
}
6973

70-
export type APIMessage = Pick<Message, 'role' | 'content'>;
74+
export type APIMessageContentPart =
75+
| {
76+
type: 'text';
77+
text: string;
78+
}
79+
| {
80+
type: 'image_url';
81+
image_url: { url: string };
82+
};
83+
84+
export type APIMessage = {
85+
role: Message['role'];
86+
content: string | APIMessageContentPart[];
87+
};
7188

7289
export interface Conversation {
7390
id: string; // format: `conv-{timestamp}`

0 commit comments

Comments
 (0)