changes
This commit is contained in:
+19
-14
@@ -46,11 +46,12 @@ const tools: Tool[] = [
|
|||||||
|
|
||||||
// --- Tool Implementations ---
|
// --- Tool Implementations ---
|
||||||
|
|
||||||
const availableTools: Record<string, Function> = {
|
const availableTools: Record<string, (args: Record<string, unknown>) => string> = {
|
||||||
get_current_time: () => {
|
get_current_time: () => {
|
||||||
return new Date().toLocaleTimeString();
|
return new Date().toLocaleTimeString();
|
||||||
},
|
},
|
||||||
calculate: ({ expression }: { expression: string }) => {
|
calculate: (args) => {
|
||||||
|
const expression = args.expression as string;
|
||||||
try {
|
try {
|
||||||
// Safety: simplistic eval for demo purposes.
|
// Safety: simplistic eval for demo purposes.
|
||||||
// In production, use a math parser library like mathjs.
|
// In production, use a math parser library like mathjs.
|
||||||
@@ -68,17 +69,19 @@ export async function chat(model: string, messages: ChatMessage[]) {
|
|||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await ollama.chat({
|
response = await ollama.chat({
|
||||||
model: model,
|
model,
|
||||||
messages: messages,
|
messages,
|
||||||
tools: tools,
|
tools,
|
||||||
});
|
});
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
// Fallback: If model doesn't support tools, retry without them
|
// Fallback: If model doesn't support tools, retry without them
|
||||||
if (e.message?.includes('does not support tools')) {
|
const errorMessage = e instanceof Error ? e.message : '';
|
||||||
|
if (errorMessage.includes('does not support tools')) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.warn(`Model ${model} does not support tools. Falling back to standard chat.`);
|
console.warn(`Model ${model} does not support tools. Falling back to standard chat.`);
|
||||||
response = await ollama.chat({
|
response = await ollama.chat({
|
||||||
model: model,
|
model,
|
||||||
messages: messages,
|
messages,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
@@ -101,6 +104,7 @@ export async function chat(model: string, messages: ChatMessage[]) {
|
|||||||
const functionToCall = availableTools[functionName];
|
const functionToCall = availableTools[functionName];
|
||||||
|
|
||||||
if (functionToCall) {
|
if (functionToCall) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(`🤖 Tool Call: ${functionName}`, tool.function.arguments);
|
console.log(`🤖 Tool Call: ${functionName}`, tool.function.arguments);
|
||||||
const functionArgs = tool.function.arguments;
|
const functionArgs = tool.function.arguments;
|
||||||
const functionResponse = functionToCall(functionArgs);
|
const functionResponse = functionToCall(functionArgs);
|
||||||
@@ -115,9 +119,9 @@ export async function chat(model: string, messages: ChatMessage[]) {
|
|||||||
|
|
||||||
// 3. Send the tool results back to the model to get the final answer
|
// 3. Send the tool results back to the model to get the final answer
|
||||||
response = await ollama.chat({
|
response = await ollama.chat({
|
||||||
model: model,
|
model,
|
||||||
messages: messages,
|
messages,
|
||||||
tools: tools,
|
tools,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +129,12 @@ export async function chat(model: string, messages: ChatMessage[]) {
|
|||||||
success: true,
|
success: true,
|
||||||
message: response.message,
|
message: response.message,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('Chat error:', error);
|
console.error('Chat error:', error);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error.message || 'Failed to generate response',
|
error: error instanceof Error ? error.message : 'Failed to generate response',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-10
@@ -144,21 +144,20 @@ export async function POST(request: NextRequest) {
|
|||||||
|
|
||||||
// Execute each tool and collect results
|
// Execute each tool and collect results
|
||||||
for (const toolCall of toolCalls) {
|
for (const toolCall of toolCalls) {
|
||||||
controller.enqueue(encoder.encode(`**Using tool: ${toolCall.name}**\n`));
|
// Use structured markers for frontend parsing
|
||||||
|
const toolOutput = await executeTool(toolCall.name, toolCall.arguments);
|
||||||
|
const toolResult = toolOutput.success
|
||||||
|
? toolOutput.result || ''
|
||||||
|
: `Error: ${toolOutput.error}`;
|
||||||
|
|
||||||
const result = await executeTool(toolCall.name, toolCall.arguments);
|
// Format: <!--TOOL_START:name:args-->result<!--TOOL_END-->
|
||||||
|
const toolMarker = `<!--TOOL_START:${toolCall.name}:${JSON.stringify(toolCall.arguments)}-->${toolResult}<!--TOOL_END-->\n\n`;
|
||||||
// Send tool result to stream
|
controller.enqueue(encoder.encode(toolMarker));
|
||||||
if (result.success) {
|
|
||||||
controller.enqueue(encoder.encode(`\`\`\`\n${result.result}\n\`\`\`\n\n`));
|
|
||||||
} else {
|
|
||||||
controller.enqueue(encoder.encode(`Error: ${result.error}\n\n`));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tool result to messages for next iteration
|
// Add tool result to messages for next iteration
|
||||||
workingMessages.push({
|
workingMessages.push({
|
||||||
role: 'tool',
|
role: 'tool',
|
||||||
content: result.success ? result.result || '' : `Error: ${result.error}`,
|
content: toolResult,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -528,6 +528,7 @@ export default function ChatLayout() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveChat = async (chatId: string) => {
|
const handleRemoveChat = async (chatId: string) => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
if (!window.confirm('Are you sure you want to delete this chat?')) {
|
if (!window.confirm('Are you sure you want to delete this chat?')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
.markdown {
|
.markdown {
|
||||||
word-wrap: break-word;
|
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,19 +13,20 @@
|
|||||||
.codeBlock {
|
.codeBlock {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-all;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fadeIn {
|
.fadeIn {
|
||||||
animation: textFadeIn 0.3s ease-out forwards;
|
animation: text-fade-in 0.3s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes textFadeIn {
|
@keyframes text-fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { ToolCallDisplay } from './ToolCallDisplay';
|
||||||
import classes from './MarkdownMessage.module.css';
|
import classes from './MarkdownMessage.module.css';
|
||||||
// Import KaTeX CSS for LaTeX rendering
|
// Import KaTeX CSS for LaTeX rendering
|
||||||
import 'katex/dist/katex.min.css';
|
import 'katex/dist/katex.min.css';
|
||||||
@@ -27,6 +28,71 @@ interface MarkdownMessageProps {
|
|||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ParsedToolCall {
|
||||||
|
toolName: string;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContentSegment {
|
||||||
|
type: 'text' | 'tool';
|
||||||
|
content?: string;
|
||||||
|
toolCall?: ParsedToolCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse content to extract tool calls and regular text segments
|
||||||
|
*/
|
||||||
|
function parseContentWithToolCalls(content: string): ContentSegment[] {
|
||||||
|
const segments: ContentSegment[] = [];
|
||||||
|
const toolPattern = /<!--TOOL_START:(\w+):(\{.*?\})-->([\s\S]*?)<!--TOOL_END-->/g;
|
||||||
|
|
||||||
|
let lastIndex = 0;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = toolPattern.exec(content)) !== null) {
|
||||||
|
// Add text before this tool call
|
||||||
|
if (match.index > lastIndex) {
|
||||||
|
const textBefore = content.slice(lastIndex, match.index).trim();
|
||||||
|
if (textBefore) {
|
||||||
|
segments.push({ type: 'text', content: textBefore });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the tool call
|
||||||
|
const toolName = match[1];
|
||||||
|
let args: Record<string, unknown> = {};
|
||||||
|
try {
|
||||||
|
args = JSON.parse(match[2]);
|
||||||
|
} catch {
|
||||||
|
// Invalid JSON, use empty args
|
||||||
|
}
|
||||||
|
const result = match[3].trim();
|
||||||
|
|
||||||
|
segments.push({
|
||||||
|
type: 'tool',
|
||||||
|
toolCall: { toolName, args, result },
|
||||||
|
});
|
||||||
|
|
||||||
|
lastIndex = match.index + match[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add remaining text after last tool call
|
||||||
|
if (lastIndex < content.length) {
|
||||||
|
const remainingText = content.slice(lastIndex).trim();
|
||||||
|
if (remainingText) {
|
||||||
|
segments.push({ type: 'text', content: remainingText });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no tool calls found, return the whole content as text
|
||||||
|
if (segments.length === 0 && content.trim()) {
|
||||||
|
segments.push({ type: 'text', content });
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook that tracks content changes and returns content with fade-in markers for new text
|
* Hook that tracks content changes and returns content with fade-in markers for new text
|
||||||
*/
|
*/
|
||||||
@@ -90,11 +156,15 @@ function escapeHtml(text: string): string {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessageProps) {
|
/**
|
||||||
const processedContent = useStreamingContent(content, isStreaming);
|
* Strip tool call markers from content for streaming display
|
||||||
|
*/
|
||||||
|
function stripToolMarkers(content: string): string {
|
||||||
|
return content.replace(/<!--TOOL_START:\w+:\{.*?\}-->/g, '').replace(/<!--TOOL_END-->/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function MarkdownContent({ content }: { content: string }) {
|
||||||
return (
|
return (
|
||||||
<div className={classes.markdown}>
|
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
remarkPlugins={[remarkGfm, remarkMath]}
|
||||||
rehypePlugins={[rehypeRaw, rehypeKatex]}
|
rehypePlugins={[rehypeRaw, rehypeKatex]}
|
||||||
@@ -247,8 +317,41 @@ export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessag
|
|||||||
span: ({ className, children }) => <span className={className}>{children}</span>,
|
span: ({ className, children }) => <span className={className}>{children}</span>,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{processedContent}
|
{content}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownMessage({ content, isStreaming = false }: MarkdownMessageProps) {
|
||||||
|
const processedContent = useStreamingContent(content, isStreaming);
|
||||||
|
|
||||||
|
// During streaming, just show the raw content (with tool markers stripped)
|
||||||
|
if (isStreaming) {
|
||||||
|
return (
|
||||||
|
<div className={classes.markdown}>
|
||||||
|
<MarkdownContent content={stripToolMarkers(processedContent)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When not streaming, parse and render tool calls
|
||||||
|
const segments = parseContentWithToolCalls(content);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.markdown}>
|
||||||
|
{segments.map((segment, index) => {
|
||||||
|
if (segment.type === 'tool' && segment.toolCall) {
|
||||||
|
return (
|
||||||
|
<ToolCallDisplay
|
||||||
|
key={`tool-${index}`}
|
||||||
|
toolName={segment.toolCall.toolName}
|
||||||
|
args={segment.toolCall.args}
|
||||||
|
result={segment.toolCall.result}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <MarkdownContent key={`text-${index}`} content={segment.content || ''} />;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
.container {
|
||||||
|
background-color: var(--mantine-color-default-hover);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
transition: background-color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header:hover {
|
||||||
|
background-color: var(--mantine-color-default-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 0 var(--mantine-spacing-xs) var(--mantine-spacing-xs);
|
||||||
|
border-top: 1px solid var(--mantine-color-default-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-top: var(--mantine-spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
font-size: var(--mantine-font-size-xs);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { IconChevronDown, IconChevronRight, IconTool } from '@tabler/icons-react';
|
||||||
|
import { ActionIcon, Code, Collapse, Group, Paper, Text, useMantineTheme } from '@mantine/core';
|
||||||
|
import { useThemeContext } from '@/components/DynamicThemeProvider';
|
||||||
|
import classes from './ToolCallDisplay.module.css';
|
||||||
|
|
||||||
|
interface ToolCallDisplayProps {
|
||||||
|
toolName: string;
|
||||||
|
args: Record<string, unknown>;
|
||||||
|
result: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Friendly tool names
|
||||||
|
const toolDisplayNames: Record<string, string> = {
|
||||||
|
calculator: 'Calculator',
|
||||||
|
get_current_datetime: 'Date/Time',
|
||||||
|
fetch_url: 'Fetch URL',
|
||||||
|
web_search: 'Web Search',
|
||||||
|
execute_code: 'Code Execution',
|
||||||
|
read_file: 'Read File',
|
||||||
|
write_file: 'Write File',
|
||||||
|
get_weather: 'Weather',
|
||||||
|
generate_image: 'Image Generation',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ToolCallDisplay({ toolName, args, result }: ToolCallDisplayProps) {
|
||||||
|
const [opened, setOpened] = useState(false);
|
||||||
|
const { primaryColor } = useThemeContext();
|
||||||
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
const displayName = toolDisplayNames[toolName] || toolName;
|
||||||
|
const isError = result.startsWith('Error:');
|
||||||
|
|
||||||
|
// Format args for display
|
||||||
|
const argsDisplay = Object.entries(args)
|
||||||
|
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper className={classes.container} withBorder radius="sm" p={0} my="xs">
|
||||||
|
<Group
|
||||||
|
className={classes.header}
|
||||||
|
onClick={() => setOpened(!opened)}
|
||||||
|
gap="xs"
|
||||||
|
wrap="nowrap"
|
||||||
|
p="xs"
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<ActionIcon variant="subtle" color={primaryColor} size="xs">
|
||||||
|
{opened ? <IconChevronDown size={14} /> : <IconChevronRight size={14} />}
|
||||||
|
</ActionIcon>
|
||||||
|
<IconTool size={16} color={theme.colors[primaryColor][6]} />
|
||||||
|
<Text size="sm" fw={500} c={primaryColor}>
|
||||||
|
{displayName}
|
||||||
|
</Text>
|
||||||
|
{!opened && argsDisplay && (
|
||||||
|
<Text size="xs" c="dimmed" truncate style={{ flex: 1 }}>
|
||||||
|
{argsDisplay}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Collapse in={opened}>
|
||||||
|
<div className={classes.content}>
|
||||||
|
{Object.keys(args).length > 0 && (
|
||||||
|
<div className={classes.section}>
|
||||||
|
<Text size="xs" c="dimmed" mb={4}>
|
||||||
|
Arguments:
|
||||||
|
</Text>
|
||||||
|
<Code block className={classes.code}>
|
||||||
|
{JSON.stringify(args, null, 2)}
|
||||||
|
</Code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={classes.section}>
|
||||||
|
<Text size="xs" c="dimmed" mb={4}>
|
||||||
|
Result:
|
||||||
|
</Text>
|
||||||
|
<Code block className={classes.code} c={isError ? 'red' : undefined}>
|
||||||
|
{result}
|
||||||
|
</Code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -35,19 +35,22 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama';
|
import { deleteModel, getInstalledModels, pullModel, type OllamaModel } from '@/app/actions/ollama';
|
||||||
|
|
||||||
const POPULAR_MODELS = [
|
/*
|
||||||
'llama3.2',
|
* Popular models list - reserved for future autocomplete feature
|
||||||
'llama3.1',
|
* const POPULAR_MODELS = [
|
||||||
'mistral',
|
* 'llama3.2',
|
||||||
'gemma2',
|
* 'llama3.1',
|
||||||
'qwen2.5',
|
* 'mistral',
|
||||||
'phi3.5',
|
* 'gemma2',
|
||||||
'neural-chat',
|
* 'qwen2.5',
|
||||||
'starling-lm',
|
* 'phi3.5',
|
||||||
'codellama',
|
* 'neural-chat',
|
||||||
'deepseek-coder',
|
* 'starling-lm',
|
||||||
'llava',
|
* 'codellama',
|
||||||
];
|
* 'deepseek-coder',
|
||||||
|
* 'llava',
|
||||||
|
* ];
|
||||||
|
*/
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -98,8 +101,6 @@ export function SettingsModal({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [value, setValue] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Filter installed models based on search
|
// Filter installed models based on search
|
||||||
const options = models
|
const options = models
|
||||||
.filter((item) => item.name.toLowerCase().includes(search.toLowerCase().trim()))
|
.filter((item) => item.name.toLowerCase().includes(search.toLowerCase().trim()))
|
||||||
@@ -129,6 +130,7 @@ export function SettingsModal({
|
|||||||
setUser(null);
|
setUser(null);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -139,6 +141,7 @@ export function SettingsModal({
|
|||||||
const list = await getInstalledModels();
|
const list = await getInstalledModels();
|
||||||
setModels(list);
|
setModels(list);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingModels(false);
|
setLoadingModels(false);
|
||||||
@@ -146,7 +149,9 @@ export function SettingsModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePullModel = async () => {
|
const handlePullModel = async () => {
|
||||||
if (!newModelName) return;
|
if (!newModelName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setPullingModel(newModelName);
|
setPullingModel(newModelName);
|
||||||
try {
|
try {
|
||||||
const result = await pullModel(newModelName);
|
const result = await pullModel(newModelName);
|
||||||
@@ -157,6 +162,7 @@ export function SettingsModal({
|
|||||||
setError(result.message);
|
setError(result.message);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
setPullingModel(null);
|
setPullingModel(null);
|
||||||
@@ -164,11 +170,15 @@ export function SettingsModal({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteModel = async (name: string) => {
|
const handleDeleteModel = async (name: string) => {
|
||||||
if (!confirm(`Are you sure you want to delete ${name}?`)) return;
|
// eslint-disable-next-line no-alert
|
||||||
|
if (!confirm(`Are you sure you want to delete ${name}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await deleteModel(name);
|
await deleteModel(name);
|
||||||
await fetchModels();
|
await fetchModels();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -195,8 +205,8 @@ export function SettingsModal({
|
|||||||
await fetchUser();
|
await fetchUser();
|
||||||
setUsername('');
|
setUsername('');
|
||||||
setPassword('');
|
setPassword('');
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
setError(err.message);
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -220,6 +230,7 @@ export function SettingsModal({
|
|||||||
body: JSON.stringify({ accentColor: color }),
|
body: JSON.stringify({ accentColor: color }),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error('Failed to save accent color:', e);
|
console.error('Failed to save accent color:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const calculatorTool: Tool = {
|
|||||||
// Safe math evaluation using Function constructor with limited scope
|
// Safe math evaluation using Function constructor with limited scope
|
||||||
function safeEvaluate(expression: string): number {
|
function safeEvaluate(expression: string): number {
|
||||||
// Replace common math notation
|
// Replace common math notation
|
||||||
let expr = expression
|
const expr = expression
|
||||||
.replace(/\^/g, '**') // Exponents
|
.replace(/\^/g, '**') // Exponents
|
||||||
.replace(/sqrt/g, 'Math.sqrt')
|
.replace(/sqrt/g, 'Math.sqrt')
|
||||||
.replace(/sin/g, 'Math.sin')
|
.replace(/sin/g, 'Math.sin')
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ export const codeExecutionHandler: ToolHandler = async (args): Promise<ToolResul
|
|||||||
const logs: string[] = [];
|
const logs: string[] = [];
|
||||||
const mockConsole = {
|
const mockConsole = {
|
||||||
log: (...args: unknown[]) => logs.push(args.map(String).join(' ')),
|
log: (...args: unknown[]) => logs.push(args.map(String).join(' ')),
|
||||||
error: (...args: unknown[]) => logs.push('[ERROR] ' + args.map(String).join(' ')),
|
error: (...args: unknown[]) => logs.push(`[ERROR] ${args.map(String).join(' ')}`),
|
||||||
warn: (...args: unknown[]) => logs.push('[WARN] ' + args.map(String).join(' ')),
|
warn: (...args: unknown[]) => logs.push(`[WARN] ${args.map(String).join(' ')}`),
|
||||||
info: (...args: unknown[]) => logs.push('[INFO] ' + args.map(String).join(' ')),
|
info: (...args: unknown[]) => logs.push(`[INFO] ${args.map(String).join(' ')}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a sandboxed context with limited globals
|
// Create a sandboxed context with limited globals
|
||||||
@@ -87,7 +87,6 @@ export const codeExecutionHandler: ToolHandler = async (args): Promise<ToolResul
|
|||||||
return fn.call(sandbox);
|
return fn.call(sandbox);
|
||||||
};
|
};
|
||||||
|
|
||||||
let result: unknown;
|
|
||||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||||
setTimeout(() => reject(new Error('Execution timed out')), timeoutMs);
|
setTimeout(() => reject(new Error('Execution timed out')), timeoutMs);
|
||||||
});
|
});
|
||||||
@@ -100,15 +99,15 @@ export const codeExecutionHandler: ToolHandler = async (args): Promise<ToolResul
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
result = await Promise.race([executionPromise, timeoutPromise]);
|
const result = await Promise.race([executionPromise, timeoutPromise]);
|
||||||
|
|
||||||
// Format output
|
// Format output
|
||||||
let output = '';
|
let output = '';
|
||||||
if (logs.length > 0) {
|
if (logs.length > 0) {
|
||||||
output += 'Console output:\n' + logs.join('\n') + '\n\n';
|
output += `Console output:\n${logs.join('\n')}\n\n`;
|
||||||
}
|
}
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
output += 'Result: ' + JSON.stringify(result, null, 2);
|
output += `Result: ${JSON.stringify(result, null, 2)}`;
|
||||||
} else if (logs.length === 0) {
|
} else if (logs.length === 0) {
|
||||||
output = 'Code executed successfully (no output)';
|
output = 'Code executed successfully (no output)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const dateTimeHandler: ToolHandler = async (args): Promise<ToolResult> =>
|
|||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'iso':
|
case 'iso':
|
||||||
result = now.toLocaleString('sv-SE', { timeZone: timezone }).replace(' ', 'T') + 'Z';
|
result = `${now.toLocaleString('sv-SE', { timeZone: timezone }).replace(' ', 'T')}Z`;
|
||||||
break;
|
break;
|
||||||
case 'date_only':
|
case 'date_only':
|
||||||
result = now.toLocaleDateString('en-US', {
|
result = now.toLocaleDateString('en-US', {
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ export const imageGenerationTool: Tool = {
|
|||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: 'generate_image',
|
name: 'generate_image',
|
||||||
description:
|
description: `Generate an image based on a text description. Creates images using AI image generation. ${
|
||||||
'Generate an image based on a text description. Creates images using AI image generation. ' +
|
IMAGE_API_URL
|
||||||
(IMAGE_API_URL
|
|
||||||
? 'Image generation is available.'
|
? 'Image generation is available.'
|
||||||
: 'NOTE: Image generation is not configured. Set IMAGE_GENERATION_API_URL environment variable.'),
|
: 'NOTE: Image generation is not configured. Set IMAGE_GENERATION_API_URL environment variable.'
|
||||||
|
}`,
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ import { webSearchHandler, webSearchTool } from './web-search';
|
|||||||
export type { ToolHandler, ToolResult } from './types';
|
export type { ToolHandler, ToolResult } from './types';
|
||||||
|
|
||||||
// Registry of tool handlers
|
// Registry of tool handlers
|
||||||
const toolHandlers: Map<string, ToolHandler> = new Map();
|
const toolHandlers = new Map<string, ToolHandler>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a tool handler
|
* Register a tool handler
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const urlFetchHandler: ToolHandler = async (args): Promise<ToolResult> =>
|
|||||||
|
|
||||||
// Truncate if needed
|
// Truncate if needed
|
||||||
if (text.length > maxLength) {
|
if (text.length > maxLength) {
|
||||||
text = text.substring(0, maxLength) + '\n\n[Content truncated...]';
|
text = `${text.substring(0, maxLength)}\n\n[Content truncated...]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user