Compare commits

...

18 Commits

Author SHA1 Message Date
Willie Zutz
c8def1989a fix(Library): Returns metadata back to original format so sources continue to work. 2025-05-01 12:17:08 -06:00
Willie Zutz
a71e4ae10d feat(app):
- Adds true chat mode. Moves writing mode to local research mode.
- Adds model stats that shows model name and response time for messages.
- Adds settings toggle to allow turning off automatic suggestions
2025-05-01 11:32:13 -06:00
Willie Zutz
abf9dbb8ba Merge remote-tracking branch 'upstream/master' 2025-04-29 10:23:52 -06:00
Willie Zutz
b3aafba30c Updates yarn.lock 2025-04-20 13:52:40 -06:00
Willie Zutz
9f7fd178e0 Cleans up unnecessary file. 2025-04-20 13:15:40 -06:00
Willie Zutz
59a10d7d00 Ran prettier formatting 2025-04-20 13:12:23 -06:00
Willie Zutz
67ee9eff53 Apply context window everywhere. Ensure styling is good on all screen sizes. Cleanup inconsistencies with upstream branch. 2025-04-20 13:10:59 -06:00
Willie Zutz
0bb860b154 Fixes history rewrite bug 2025-04-20 11:57:48 -06:00
Willie Zutz
c0705d1d9e Support for Ollama context window configuration 2025-04-20 01:37:10 -06:00
Willie Zutz
73b5e8832e Removed compact mode 2025-04-19 13:36:50 -06:00
Willie Zutz
b2da9faeed More merge 2025-04-19 12:52:15 -06:00
Willie Zutz
1a2ad8a59d Merge remote-tracking branch 'upstream/master' 2025-04-19 12:51:57 -06:00
Willie Zutz
e0817d1008 Merge branch 'master' of github.com:ItzCrazyKns/Perplexica 2025-03-06 22:03:19 -07:00
Willie Zutz
690ef42861 Fixes a bug with rewriting where history wouldn't get removed. 2025-02-17 01:22:34 -07:00
Willie Zutz
b84e4e4ce6 Added an icon to indicate that compact mode is enabled. 2025-02-16 15:08:30 -07:00
Willie Zutz
467905d9f2 Added compact mode for more concise answers.
Made optimization mode persist between page refreshes.
Added mode switcher to chat so it can be changed while researching.
2025-02-16 15:02:05 -07:00
Willie Zutz
18b6f5b674 Updated formatting 2025-02-15 16:07:19 -07:00
Willie Zutz
2bdcbf20fb User customizable context window for ollama models. 2025-02-15 16:03:24 -07:00
27 changed files with 13746 additions and 1884 deletions

View File

@@ -41,9 +41,10 @@ Want to know more about its architecture and how it works? You can read it [here
- **Two Main Modes:**
- **Copilot Mode:** (In development) Boosts search by generating different queries to find more relevant internet sources. Like normal search instead of just using the context by SearxNG, it visits the top matches and tries to find relevant sources to the user's query directly from the page.
- **Normal Mode:** Processes your query and performs a web search.
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 6 focus modes:
- **Focus Modes:** Special modes to better answer specific types of questions. Perplexica currently has 7 focus modes:
- **All Mode:** Searches the entire web to find the best results.
- **Writing Assistant Mode:** Helpful for writing tasks that do not require searching the web.
- **Local Research Mode:** Research and interact with local files with citations.
- **Chat Mode:** Have a truly creative conversation without web search.
- **Academic Search Mode:** Finds articles and papers, ideal for academic research.
- **YouTube Search Mode:** Finds YouTube videos based on the search query.
- **Wolfram Alpha Search Mode:** Answers queries that need calculations or data analysis using Wolfram Alpha.

View File

@@ -55,7 +55,7 @@ The API accepts a JSON object in the request body, where you define the focus mo
- **`focusMode`** (string, required): Specifies which focus mode to use. Available modes:
- `webSearch`, `academicSearch`, `writingAssistant`, `wolframAlphaSearch`, `youtubeSearch`, `redditSearch`.
- `webSearch`, `academicSearch`, `localResearch`, `chat`, `wolframAlphaSearch`, `youtubeSearch`, `redditSearch`.
- **`optimizationMode`** (string, optional): Specifies the optimization mode to control the balance between performance and quality. Available modes:

11024
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
"@langchain/core": "^0.3.42",
"@langchain/google-genai": "^0.1.12",
"@langchain/openai": "^0.0.25",
"@langchain/ollama": "^0.2.0",
"@langchain/textsplitters": "^0.1.0",
"@tailwindcss/typography": "^0.5.12",
"@xenova/transformers": "^2.17.2",

View File

@@ -20,6 +20,7 @@ import {
getCustomOpenaiApiUrl,
getCustomOpenaiModelName,
} from '@/lib/config';
import { ChatOllama } from '@langchain/ollama';
import { searchHandlers } from '@/lib/search';
export const runtime = 'nodejs';
@@ -34,6 +35,7 @@ type Message = {
type ChatModel = {
provider: string;
name: string;
ollamaContextWindow?: number;
};
type EmbeddingModel = {
@@ -52,12 +54,18 @@ type Body = {
systemInstructions: string;
};
type ModelStats = {
modelName: string;
responseTime?: number;
};
const handleEmitterEvents = async (
stream: EventEmitter,
writer: WritableStreamDefaultWriter,
encoder: TextEncoder,
aiMessageId: string,
chatId: string,
startTime: number,
) => {
let recievedMessage = '';
let sources: any[] = [];
@@ -90,12 +98,32 @@ const handleEmitterEvents = async (
sources = parsedData.data;
}
});
let modelStats: ModelStats = {
modelName: '',
};
stream.on('stats', (data) => {
const parsedData = JSON.parse(data);
if (parsedData.type === 'modelStats') {
modelStats = parsedData.data;
}
});
stream.on('end', () => {
const endTime = Date.now();
const duration = endTime - startTime;
modelStats = {
...modelStats,
responseTime: duration,
};
writer.write(
encoder.encode(
JSON.stringify({
type: 'messageEnd',
messageId: aiMessageId,
modelStats: modelStats,
}) + '\n',
),
);
@@ -110,6 +138,7 @@ const handleEmitterEvents = async (
metadata: JSON.stringify({
createdAt: new Date(),
...(sources && sources.length > 0 && { sources }),
modelStats: modelStats,
}),
})
.execute();
@@ -183,6 +212,7 @@ const handleHistorySave = async (
export const POST = async (req: Request) => {
try {
const startTime = Date.now();
const body = (await req.json()) as Body;
const { message } = body;
@@ -232,6 +262,11 @@ export const POST = async (req: Request) => {
}) as unknown as BaseChatModel;
} else if (chatModelProvider && chatModel) {
llm = chatModel.model;
// Set context window size for Ollama models
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
}
if (!llm) {
@@ -286,7 +321,14 @@ export const POST = async (req: Request) => {
const writer = responseStream.writable.getWriter();
const encoder = new TextEncoder();
handleEmitterEvents(stream, writer, encoder, aiMessageId, message.chatId);
handleEmitterEvents(
stream,
writer,
encoder,
aiMessageId,
message.chatId,
startTime,
);
handleHistorySave(message, humanMessageId, body.focusMode, body.files);
return new Response(responseStream.readable, {

View File

@@ -7,11 +7,13 @@ import {
import { getAvailableChatModelProviders } from '@/lib/providers';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { ChatOllama } from '@langchain/ollama';
import { ChatOpenAI } from '@langchain/openai';
interface ChatModel {
provider: string;
model: string;
ollamaContextWindow?: number;
}
interface ImageSearchBody {
@@ -58,6 +60,10 @@ export const POST = async (req: Request) => {
}) as unknown as BaseChatModel;
} else if (chatModelProvider && chatModel) {
llm = chatModel.model;
// Set context window size for Ollama models
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
}
if (!llm) {

View File

@@ -13,12 +13,14 @@ import {
getCustomOpenaiModelName,
} from '@/lib/config';
import { searchHandlers } from '@/lib/search';
import { ChatOllama } from '@langchain/ollama';
interface chatModel {
provider: string;
name: string;
customOpenAIKey?: string;
customOpenAIBaseURL?: string;
ollamaContextWindow?: number;
}
interface embeddingModel {
@@ -97,6 +99,10 @@ export const POST = async (req: Request) => {
.model as unknown as BaseChatModel | undefined;
}
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
if (
embeddingModelProviders[embeddingModelProvider] &&
embeddingModelProviders[embeddingModelProvider][embeddingModel]

View File

@@ -8,10 +8,12 @@ import { getAvailableChatModelProviders } from '@/lib/providers';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { ChatOpenAI } from '@langchain/openai';
import { ChatOllama } from '@langchain/ollama';
interface ChatModel {
provider: string;
model: string;
ollamaContextWindow?: number;
}
interface SuggestionsGenerationBody {
@@ -57,6 +59,10 @@ export const POST = async (req: Request) => {
}) as unknown as BaseChatModel;
} else if (chatModelProvider && chatModel) {
llm = chatModel.model;
// Set context window size for Ollama models
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
}
if (!llm) {

View File

@@ -7,11 +7,13 @@ import {
import { getAvailableChatModelProviders } from '@/lib/providers';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages';
import { ChatOllama } from '@langchain/ollama';
import { ChatOpenAI } from '@langchain/openai';
interface ChatModel {
provider: string;
model: string;
ollamaContextWindow?: number;
}
interface VideoSearchBody {
@@ -58,6 +60,10 @@ export const POST = async (req: Request) => {
}) as unknown as BaseChatModel;
} else if (chatModelProvider && chatModel) {
llm = chatModel.model;
// Set context window size for Ollama models
if (llm instanceof ChatOllama && body.chatModel?.provider === 'ollama') {
llm.numCtx = body.chatModel.ollamaContextWindow || 2048;
}
}
if (!llm) {

View File

@@ -5,7 +5,7 @@ import { useEffect, useState } from 'react';
import { cn } from '@/lib/utils';
import { Switch } from '@headlessui/react';
import ThemeSwitcher from '@/components/theme/Switcher';
import { ImagesIcon, VideoIcon } from 'lucide-react';
import { ImagesIcon, VideoIcon, Layers3 } from 'lucide-react';
import Link from 'next/link';
import { PROVIDER_METADATA } from '@/lib/providers';
@@ -26,6 +26,7 @@ interface SettingsType {
customOpenaiApiKey: string;
customOpenaiApiUrl: string;
customOpenaiModelName: string;
ollamaContextWindow: number;
}
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
@@ -146,8 +147,14 @@ const Page = () => {
const [isLoading, setIsLoading] = useState(false);
const [automaticImageSearch, setAutomaticImageSearch] = useState(false);
const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false);
const [automaticSuggestions, setAutomaticSuggestions] = useState(true);
const [systemInstructions, setSystemInstructions] = useState<string>('');
const [savingStates, setSavingStates] = useState<Record<string, boolean>>({});
const [contextWindowSize, setContextWindowSize] = useState(2048);
const [isCustomContextWindow, setIsCustomContextWindow] = useState(false);
const predefinedContextSizes = [
1024, 2048, 3072, 4096, 8192, 16384, 32768, 65536, 131072,
];
useEffect(() => {
const fetchConfig = async () => {
@@ -159,6 +166,7 @@ const Page = () => {
});
const data = (await res.json()) as SettingsType;
setConfig(data);
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
@@ -207,6 +215,16 @@ const Page = () => {
setAutomaticVideoSearch(
localStorage.getItem('autoVideoSearch') === 'true',
);
setAutomaticSuggestions(
localStorage.getItem('autoSuggestions') !== 'false', // default to true if not set
);
const storedContextWindow = parseInt(
localStorage.getItem('ollamaContextWindow') ?? '2048',
);
setContextWindowSize(storedContextWindow);
setIsCustomContextWindow(
!predefinedContextSizes.includes(storedContextWindow),
);
setSystemInstructions(localStorage.getItem('systemInstructions')!);
@@ -358,6 +376,8 @@ const Page = () => {
localStorage.setItem('autoImageSearch', value.toString());
} else if (key === 'automaticVideoSearch') {
localStorage.setItem('autoVideoSearch', value.toString());
} else if (key === 'automaticSuggestions') {
localStorage.setItem('autoSuggestions', value.toString());
} else if (key === 'chatModelProvider') {
localStorage.setItem('chatModelProvider', value);
} else if (key === 'chatModel') {
@@ -366,6 +386,8 @@ const Page = () => {
localStorage.setItem('embeddingModelProvider', value);
} else if (key === 'embeddingModel') {
localStorage.setItem('embeddingModel', value);
} else if (key === 'ollamaContextWindow') {
localStorage.setItem('ollamaContextWindow', value.toString());
} else if (key === 'systemInstructions') {
localStorage.setItem('systemInstructions', value);
}
@@ -510,6 +532,47 @@ const Page = () => {
/>
</Switch>
</div>
<div className="flex items-center justify-between p-3 bg-light-secondary dark:bg-dark-secondary rounded-lg hover:bg-light-200 dark:hover:bg-dark-200 transition-colors">
<div className="flex items-center space-x-3">
<div className="p-2 bg-light-200 dark:bg-dark-200 rounded-lg">
<Layers3
size={18}
className="text-black/70 dark:text-white/70"
/>
</div>
<div>
<p className="text-sm text-black/90 dark:text-white/90 font-medium">
Automatic Suggestions
</p>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
Automatically show related suggestions after responses
</p>
</div>
</div>
<Switch
checked={automaticSuggestions}
onChange={(checked) => {
setAutomaticSuggestions(checked);
saveConfig('automaticSuggestions', checked);
}}
className={cn(
automaticSuggestions
? 'bg-[#24A0ED]'
: 'bg-light-200 dark:bg-dark-200',
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none',
)}
>
<span
className={cn(
automaticSuggestions
? 'translate-x-6'
: 'translate-x-1',
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
)}
/>
</Switch>
</div>
</div>
</SettingsSection>
@@ -599,6 +662,78 @@ const Page = () => {
];
})()}
/>
{selectedChatModelProvider === 'ollama' && (
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">
Chat Context Window Size
</p>
<Select
value={
isCustomContextWindow
? 'custom'
: contextWindowSize.toString()
}
onChange={(e) => {
const value = e.target.value;
if (value === 'custom') {
setIsCustomContextWindow(true);
} else {
setIsCustomContextWindow(false);
const numValue = parseInt(value);
setContextWindowSize(numValue);
setConfig((prev) => ({
...prev!,
ollamaContextWindow: numValue,
}));
saveConfig('ollamaContextWindow', numValue);
}
}}
options={[
...predefinedContextSizes.map((size) => ({
value: size.toString(),
label: `${size.toLocaleString()} tokens`,
})),
{ value: 'custom', label: 'Custom...' },
]}
/>
{isCustomContextWindow && (
<div className="mt-2">
<Input
type="number"
min={512}
value={contextWindowSize}
placeholder="Custom context window size (minimum 512)"
isSaving={savingStates['ollamaContextWindow']}
onChange={(e) => {
// Allow any value to be typed
const value =
parseInt(e.target.value) ||
contextWindowSize;
setContextWindowSize(value);
}}
onSave={(value) => {
// Validate only when saving
const numValue = Math.max(
512,
parseInt(value) || 2048,
);
setContextWindowSize(numValue);
setConfig((prev) => ({
...prev!,
ollamaContextWindow: numValue,
}));
saveConfig('ollamaContextWindow', numValue);
}}
/>
</div>
)}
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
{isCustomContextWindow
? 'Adjust the context window size for Ollama models (minimum 512 tokens)'
: 'Adjust the context window size for Ollama models'}
</p>
</div>
)}
</div>
)}
</div>

View File

@@ -16,9 +16,18 @@ const Chat = ({
setFileIds,
files,
setFiles,
optimizationMode,
setOptimizationMode,
}: {
messages: Message[];
sendMessage: (message: string) => void;
sendMessage: (
message: string,
options?: {
messageId?: string;
rewriteIndex?: number;
suggestions?: string[];
},
) => void;
loading: boolean;
messageAppeared: boolean;
rewrite: (messageId: string) => void;
@@ -26,6 +35,8 @@ const Chat = ({
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
const [dividerWidth, setDividerWidth] = useState(0);
const dividerRef = useRef<HTMLDivElement | null>(null);
@@ -99,6 +110,8 @@ const Chat = ({
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</div>
)}

View File

@@ -13,6 +13,11 @@ import { Settings } from 'lucide-react';
import Link from 'next/link';
import NextError from 'next/error';
export type ModelStats = {
modelName: string;
responseTime?: number;
};
export type Message = {
messageId: string;
chatId: string;
@@ -21,6 +26,7 @@ export type Message = {
role: 'user' | 'assistant';
suggestions?: string[];
sources?: Document[];
modelStats?: ModelStats;
};
export interface File {
@@ -287,6 +293,16 @@ const ChatWindow = ({ id }: { id?: string }) => {
const [notFound, setNotFound] = useState(false);
useEffect(() => {
const savedOptimizationMode = localStorage.getItem('optimizationMode');
if (savedOptimizationMode !== null) {
setOptimizationMode(savedOptimizationMode);
} else {
localStorage.setItem('optimizationMode', optimizationMode);
}
}, []);
useEffect(() => {
if (
chatId &&
@@ -327,7 +343,27 @@ const ChatWindow = ({ id }: { id?: string }) => {
}
}, [isMessagesLoaded, isConfigReady]);
const sendMessage = async (message: string, messageId?: string) => {
const sendMessage = async (
message: string,
options?: {
messageId?: string;
rewriteIndex?: number;
suggestions?: string[];
},
) => {
// Special case: If we're just updating an existing message with suggestions
if (options?.suggestions && options.messageId) {
setMessages((prev) =>
prev.map((msg) => {
if (msg.messageId === options.messageId) {
return { ...msg, suggestions: options.suggestions };
}
return msg;
}),
);
return;
}
if (loading) return;
if (!isConfigReady) {
toast.error('Cannot send message before the configuration is ready');
@@ -340,8 +376,23 @@ const ChatWindow = ({ id }: { id?: string }) => {
let sources: Document[] | undefined = undefined;
let recievedMessage = '';
let added = false;
let messageChatHistory = chatHistory;
messageId = messageId ?? crypto.randomBytes(7).toString('hex');
if (options?.rewriteIndex !== undefined) {
const rewriteIndex = options.rewriteIndex;
setMessages((prev) => {
return [...prev.slice(0, messages.length > 2 ? rewriteIndex - 1 : 0)];
});
messageChatHistory = chatHistory.slice(
0,
messages.length > 2 ? rewriteIndex - 1 : 0,
);
setChatHistory(messageChatHistory);
}
const messageId =
options?.messageId ?? crypto.randomBytes(7).toString('hex');
setMessages((prevMessages) => [
...prevMessages,
@@ -391,6 +442,9 @@ const ChatWindow = ({ id }: { id?: string }) => {
role: 'assistant',
sources: sources,
createdAt: new Date(),
modelStats: {
modelName: data.modelName,
},
},
]);
added = true;
@@ -417,12 +471,27 @@ const ChatWindow = ({ id }: { id?: string }) => {
['assistant', recievedMessage],
]);
// Always update the message, adding modelStats if available
setMessages((prev) =>
prev.map((message) => {
if (message.messageId === data.messageId) {
return {
...message,
// Include model stats if available, otherwise null
modelStats: data.modelStats || null,
};
}
return message;
}),
);
setLoading(false);
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
const autoImageSearch = localStorage.getItem('autoImageSearch');
const autoVideoSearch = localStorage.getItem('autoVideoSearch');
const autoSuggestions = localStorage.getItem('autoSuggestions');
if (autoImageSearch === 'true') {
document
@@ -440,7 +509,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
lastMsg.role === 'assistant' &&
lastMsg.sources &&
lastMsg.sources.length > 0 &&
!lastMsg.suggestions
!lastMsg.suggestions &&
autoSuggestions !== 'false' // Default to true if not set
) {
const suggestions = await getSuggestions(messagesRef.current);
setMessages((prev) =>
@@ -455,6 +525,9 @@ const ChatWindow = ({ id }: { id?: string }) => {
}
};
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
const res = await fetch('/api/chat', {
method: 'POST',
headers: {
@@ -471,10 +544,13 @@ const ChatWindow = ({ id }: { id?: string }) => {
files: fileIds,
focusMode: focusMode,
optimizationMode: optimizationMode,
history: chatHistory,
history: messageChatHistory,
chatModel: {
name: chatModelProvider.name,
provider: chatModelProvider.provider,
...(chatModelProvider.provider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
},
embeddingModel: {
name: embeddingModelProvider.name,
@@ -512,20 +588,14 @@ const ChatWindow = ({ id }: { id?: string }) => {
};
const rewrite = (messageId: string) => {
const index = messages.findIndex((msg) => msg.messageId === messageId);
if (index === -1) return;
const message = messages[index - 1];
setMessages((prev) => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
const messageIndex = messages.findIndex(
(msg) => msg.messageId === messageId,
);
if (messageIndex == -1) return;
sendMessage(messages[messageIndex - 1].content, {
messageId: messageId,
rewriteIndex: messageIndex,
});
setChatHistory((prev) => {
return [...prev.slice(0, messages.length > 2 ? index - 1 : 0)];
});
sendMessage(message.content, message.messageId);
};
useEffect(() => {
@@ -570,6 +640,8 @@ const ChatWindow = ({ id }: { id?: string }) => {
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</>
) : (

View File

@@ -0,0 +1,82 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import { Info } from 'lucide-react';
import { ModelStats } from '../ChatWindow';
import { cn } from '@/lib/utils';
interface ModelInfoButtonProps {
modelStats: ModelStats | null;
}
const ModelInfoButton: React.FC<ModelInfoButtonProps> = ({ modelStats }) => {
const [showPopover, setShowPopover] = useState(false);
const popoverRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
// Always render, using "Unknown" as fallback if model info isn't available
const modelName = modelStats?.modelName || 'Unknown';
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node) &&
buttonRef.current &&
!buttonRef.current.contains(event.target as Node)
) {
setShowPopover(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
return (
<div className="relative">
<button
ref={buttonRef}
className="p-1 ml-1 text-black/50 dark:text-white/50 rounded-full hover:bg-light-secondary dark:hover:bg-dark-secondary transition duration-200 hover:text-black dark:hover:text-white"
onClick={() => setShowPopover(!showPopover)}
aria-label="Show model information"
>
<Info size={14} />
</button>
{showPopover && (
<div
ref={popoverRef}
className="absolute z-10 left-6 top-0 w-64 rounded-md shadow-lg bg-white dark:bg-dark-secondary border border-light-200 dark:border-dark-200"
>
<div className="py-2 px-3">
<h4 className="text-sm font-medium mb-2 text-black dark:text-white">
Model Information
</h4>
<div className="space-y-1 text-xs">
<div className="flex justify-between">
<span className="text-black/70 dark:text-white/70">Model:</span>
<span className="text-black dark:text-white font-medium">
{modelName}
</span>
</div>
{modelStats?.responseTime && (
<div className="flex justify-between">
<span className="text-black/70 dark:text-white/70">
Response time:
</span>
<span className="text-black dark:text-white font-medium">
{(modelStats.responseTime / 1000).toFixed(2)}s
</span>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default ModelInfoButton;

View File

@@ -4,6 +4,7 @@
import React, { MutableRefObject, useEffect, useState } from 'react';
import { Message } from './ChatWindow';
import { cn } from '@/lib/utils';
import { getSuggestions } from '@/lib/actions';
import {
BookCopy,
Disc3,
@@ -11,10 +12,12 @@ import {
StopCircle,
Layers3,
Plus,
Sparkles,
} from 'lucide-react';
import Markdown, { MarkdownToJSX } from 'markdown-to-jsx';
import Copy from './MessageActions/Copy';
import Rewrite from './MessageActions/Rewrite';
import ModelInfoButton from './MessageActions/ModelInfo';
import MessageSources from './MessageSources';
import SearchImages from './SearchImages';
import SearchVideos from './SearchVideos';
@@ -42,10 +45,40 @@ const MessageBox = ({
dividerRef?: MutableRefObject<HTMLDivElement | null>;
isLast: boolean;
rewrite: (messageId: string) => void;
sendMessage: (message: string) => void;
sendMessage: (
message: string,
options?: {
messageId?: string;
rewriteIndex?: number;
suggestions?: string[];
},
) => void;
}) => {
const [parsedMessage, setParsedMessage] = useState(message.content);
const [speechMessage, setSpeechMessage] = useState(message.content);
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
const [autoSuggestions, setAutoSuggestions] = useState(
localStorage.getItem('autoSuggestions'),
);
const handleLoadSuggestions = async () => {
if (
loadingSuggestions ||
(message?.suggestions && message.suggestions.length > 0)
)
return;
setLoadingSuggestions(true);
try {
const suggestions = await getSuggestions([...history]);
// We need to update the message.suggestions property through parent component
sendMessage('', { messageId: message.messageId, suggestions });
} catch (error) {
console.error('Error loading suggestions:', error);
} finally {
setLoadingSuggestions(false);
}
};
useEffect(() => {
const citationRegex = /\[([^\]]+)\]/g;
@@ -105,6 +138,18 @@ const MessageBox = ({
setParsedMessage(processedMessage);
}, [message.content, message.sources, message.role]);
useEffect(() => {
const handleStorageChange = () => {
setAutoSuggestions(localStorage.getItem('autoSuggestions'));
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
const { speechStatus, start, stop } = useSpeech({ text: speechMessage });
const markdownOverrides: MarkdownToJSX.Options = {
@@ -149,6 +194,7 @@ const MessageBox = ({
</div>
)}
<div className="flex flex-col space-y-2">
{' '}
<div className="flex flex-row items-center space-x-2">
<Disc3
className={cn(
@@ -160,8 +206,10 @@ const MessageBox = ({
<h3 className="text-black dark:text-white font-medium text-xl">
Answer
</h3>
{message.modelStats && (
<ModelInfoButton modelStats={message.modelStats} />
)}
</div>
<Markdown
className={cn(
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] dark:prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
@@ -200,18 +248,37 @@ const MessageBox = ({
</div>
</div>
)}
{isLast &&
message.suggestions &&
message.suggestions.length > 0 &&
message.role === 'assistant' &&
!loading && (
<>
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="flex flex-col space-y-3 text-black dark:text-white">
<div className="flex flex-row items-center space-x-2 mt-4">
<Layers3 />
<h3 className="text-xl font-medium">Related</h3>
</div>
{isLast && message.role === 'assistant' && !loading && (
<>
<div className="h-px w-full bg-light-secondary dark:bg-dark-secondary" />
<div className="flex flex-col space-y-3 text-black dark:text-white">
<div className="flex flex-row items-center space-x-2 mt-4">
<Layers3 />
<h3 className="text-xl font-medium">Related</h3>{' '}
{(!autoSuggestions || autoSuggestions === 'false') &&
(!message.suggestions ||
message.suggestions.length === 0) ? (
<div className="bg-light-secondary dark:bg-dark-secondary">
<button
onClick={handleLoadSuggestions}
disabled={loadingSuggestions}
className="px-4 py-2 flex flex-row items-center justify-center space-x-2 rounded-lg bg-light-secondary dark:bg-dark-secondary hover:bg-light-200 dark:hover:bg-dark-200 transition duration-200 text-black/70 dark:text-white/70 hover:text-black dark:hover:text-white"
>
{loadingSuggestions ? (
<div className="w-4 h-4 border-2 border-t-transparent border-gray-400 dark:border-gray-500 rounded-full animate-spin" />
) : (
<Sparkles size={16} />
)}
<span>
{loadingSuggestions
? 'Loading suggestions...'
: 'Load suggestions'}
</span>
</button>
</div>
) : null}
</div>
{message.suggestions && message.suggestions.length > 0 ? (
<div className="flex flex-col space-y-3">
{message.suggestions.map((suggestion, i) => (
<div
@@ -236,9 +303,10 @@ const MessageBox = ({
</div>
))}
</div>
</div>
</>
)}
) : null}
</div>
</>
)}
</div>
</div>
<div className="lg:sticky lg:top-20 flex flex-col items-center space-y-3 w-full lg:w-3/12 z-30 h-full pb-4">

View File

@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import Attach from './MessageInputActions/Attach';
import CopilotToggle from './MessageInputActions/Copilot';
import Optimization from './MessageInputActions/Optimization';
import { File } from './ChatWindow';
import AttachSmall from './MessageInputActions/AttachSmall';
@@ -14,6 +15,8 @@ const MessageInput = ({
setFileIds,
files,
setFiles,
optimizationMode,
setOptimizationMode,
}: {
sendMessage: (message: string) => void;
loading: boolean;
@@ -21,6 +24,8 @@ const MessageInput = ({
setFileIds: (fileIds: string[]) => void;
files: File[];
setFiles: (files: File[]) => void;
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
const [copilotEnabled, setCopilotEnabled] = useState(false);
const [message, setMessage] = useState('');
@@ -40,20 +45,16 @@ const MessageInput = ({
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const activeElement = document.activeElement;
const isInputFocused =
activeElement?.tagName === 'INPUT' ||
activeElement?.tagName === 'TEXTAREA' ||
activeElement?.hasAttribute('contenteditable');
if (e.key === '/' && !isInputFocused) {
e.preventDefault();
inputRef.current?.focus();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
@@ -75,58 +76,95 @@ const MessageInput = ({
}
}}
className={cn(
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center overflow-hidden border border-light-200 dark:border-dark-200',
mode === 'multi' ? 'flex-col rounded-lg' : 'flex-row rounded-full',
'bg-light-secondary dark:bg-dark-secondary p-4 flex items-center border border-light-200 dark:border-dark-200',
mode === 'multi'
? 'flex-col rounded-lg'
: 'flex-col md:flex-row rounded-lg md:rounded-full',
)}
>
{mode === 'single' && (
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
)}
<TextareaAutosize
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onHeightChange={(height, props) => {
setTextareaRows(Math.ceil(height / props.rowHeight));
}}
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
placeholder="Ask a follow-up"
/>
{mode === 'single' && (
<div className="flex flex-row items-center space-x-4">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
<button
disabled={message.trim().length === 0 || loading}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />
</button>
</div>
)}
{mode === 'multi' && (
<div className="flex flex-row items-center justify-between w-full pt-2">
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<div className="flex flex-row items-center space-x-4">
<div className="flex flex-row items-center justify-between w-full mb-2 md:mb-0 md:w-auto">
<div className="flex flex-row items-center space-x-2">
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</div>
<div className="md:hidden">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
</div>
</div>
)}
<div className="flex flex-row items-center w-full">
<TextareaAutosize
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onHeightChange={(height, props) => {
setTextareaRows(Math.ceil(height / props.rowHeight));
}}
className="transition bg-transparent dark:placeholder:text-white/50 placeholder:text-sm text-sm dark:text-white resize-none focus:outline-none w-full px-2 max-h-24 lg:max-h-36 xl:max-h-48 flex-grow flex-shrink"
placeholder="Ask a follow-up"
/>
{mode === 'single' && (
<div className="flex flex-row items-center space-x-4">
<div className="hidden md:block">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
</div>
<button
disabled={message.trim().length === 0 || loading}
className="bg-[#24A0ED] text-white text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />
</button>
</div>
)}
</div>
{mode === 'multi' && (
<div className="flex flex-col md:flex-row items-start md:items-center justify-between w-full pt-2">
<div className="flex flex-row items-center justify-between w-full md:w-auto mb-2 md:mb-0">
<div className="flex flex-row items-center space-x-2">
<AttachSmall
fileIds={fileIds}
setFileIds={setFileIds}
files={files}
setFiles={setFiles}
/>
<Optimization
optimizationMode={optimizationMode}
setOptimizationMode={setOptimizationMode}
/>
</div>
<div className="md:hidden">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
</div>
</div>
<div className="flex flex-row items-center space-x-4 self-end">
<div className="hidden md:block">
<CopilotToggle
copilotEnabled={copilotEnabled}
setCopilotEnabled={setCopilotEnabled}
/>
</div>
<button
disabled={message.trim().length === 0 || loading}
className="bg-[#24A0ED] text-white disabled:text-black/50 dark:disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#e0e0dc79] dark:disabled:bg-[#ececec21] rounded-full p-2"
>
<ArrowUp className="bg-background" size={17} />
</button>

View File

@@ -2,6 +2,7 @@ import {
BadgePercent,
ChevronDown,
Globe,
MessageCircle,
Pencil,
ScanEye,
SwatchBook,
@@ -30,11 +31,23 @@ const focusModes = [
icon: <SwatchBook size={20} />,
},
{
key: 'writingAssistant',
title: 'Writing',
description: 'Chat without searching the web',
key: 'chat',
title: 'Chat',
description: 'Have a creative conversation',
icon: <MessageCircle size={16} />,
},
{
key: 'localResearch',
title: 'Local Research',
description: 'Research and interact with local files with citations',
icon: <Pencil size={16} />,
},
{
key: 'redditSearch',
title: 'Reddit',
description: 'Search for discussions and opinions',
icon: <SiReddit className="h-5 w-auto mr-0.5" />,
},
{
key: 'wolframAlphaSearch',
title: 'Wolfram Alpha',
@@ -47,12 +60,6 @@ const focusModes = [
description: 'Search and watch videos',
icon: <SiYoutube className="h-5 w-auto mr-0.5" />,
},
{
key: 'redditSearch',
title: 'Reddit',
description: 'Search for discussions and opinions',
icon: <SiReddit className="h-5 w-auto mr-0.5" />,
},
];
const Focus = ({

View File

@@ -1,4 +1,4 @@
import { ChevronDown, Sliders, Star, Zap } from 'lucide-react';
import { ChevronDown, Minimize2, Sliders, Star, Zap } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Popover,
@@ -7,7 +7,6 @@ import {
Transition,
} from '@headlessui/react';
import { Fragment } from 'react';
const OptimizationModes = [
{
key: 'speed',
@@ -41,8 +40,13 @@ const Optimization = ({
optimizationMode: string;
setOptimizationMode: (mode: string) => void;
}) => {
const handleOptimizationChange = (mode: string) => {
setOptimizationMode(mode);
localStorage.setItem('optimizationMode', mode);
};
return (
<Popover className="relative w-full max-w-[15rem] md:max-w-md lg:max-w-lg">
<Popover className="relative">
<PopoverButton
type="button"
className="p-2 text-black/50 dark:text-white/50 rounded-xl hover:bg-light-secondary dark:hover:bg-dark-secondary active:scale-95 transition duration-200 hover:text-black dark:hover:text-white"
@@ -70,11 +74,11 @@ const Optimization = ({
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-10 w-64 md:w-[250px] right-0">
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-full p-4 max-h-[200px] md:max-h-none overflow-y-auto">
<PopoverPanel className="absolute z-10 bottom-[100%] mb-2 left-1/2 transform -translate-x-1/2">
<div className="flex flex-col gap-2 bg-light-primary dark:bg-dark-primary border rounded-lg border-light-200 dark:border-dark-200 w-max max-w-[300px] p-4 max-h-[200px] md:max-h-none overflow-y-auto">
{OptimizationModes.map((mode, i) => (
<PopoverButton
onClick={() => setOptimizationMode(mode.key)}
onClick={() => handleOptimizationChange(mode.key)}
key={i}
disabled={mode.key === 'quality'}
className={cn(

View File

@@ -35,9 +35,10 @@ const SearchImages = ({
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
const res = await fetch(`/api/images`, {
method: 'POST',
@@ -54,6 +55,9 @@ const SearchImages = ({
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
...(chatModelProvider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
},
}),
});

View File

@@ -50,9 +50,10 @@ const Searchvideos = ({
const chatModelProvider = localStorage.getItem('chatModelProvider');
const chatModel = localStorage.getItem('chatModel');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
const res = await fetch(`/api/videos`, {
method: 'POST',
@@ -69,6 +70,9 @@ const Searchvideos = ({
customOpenAIBaseURL: customOpenAIBaseURL,
customOpenAIKey: customOpenAIKey,
}),
...(chatModelProvider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
},
}),
});

View File

@@ -6,6 +6,8 @@ export const getSuggestions = async (chatHisory: Message[]) => {
const customOpenAIKey = localStorage.getItem('openAIApiKey');
const customOpenAIBaseURL = localStorage.getItem('openAIBaseURL');
const ollamaContextWindow =
localStorage.getItem('ollamaContextWindow') || '2048';
const res = await fetch(`/api/suggestions`, {
method: 'POST',
@@ -21,6 +23,9 @@ export const getSuggestions = async (chatHisory: Message[]) => {
customOpenAIKey,
customOpenAIBaseURL,
}),
...(chatModelProvider === 'ollama' && {
ollamaContextWindow: parseInt(ollamaContextWindow),
}),
},
}),
});

19
src/lib/prompts/chat.ts Normal file
View File

@@ -0,0 +1,19 @@
export const chatPrompt = `
You are Perplexica, an AI model who is expert at having creative conversations with users. You are currently set on focus mode 'Chat', which means you will engage in a truly creative conversation without searching the web or citing sources.
In Chat mode, you should be:
- Creative and engaging in your responses
- Helpful and informative based on your internal knowledge
- Conversational and natural in your tone
- Willing to explore ideas, hypothetical scenarios, and creative topics
Since you are in Chat mode, you would not perform web searches or cite sources. If the user asks a question that would benefit from web search or specific data, you can suggest they switch to a different focus mode like 'All Mode' for general web search or another specialized mode.
### User instructions
These instructions are shared to you by the user and not by the system. You will have to follow them but give them less priority than the above instructions. If the user has provided specific instructions or preferences, incorporate them into your response while adhering to the overall guidelines.
{systemInstructions}
<context>
{context}
</context>
`;

View File

@@ -11,7 +11,8 @@ import {
wolframAlphaSearchResponsePrompt,
wolframAlphaSearchRetrieverPrompt,
} from './wolframAlpha';
import { writingAssistantPrompt } from './writingAssistant';
import { localResearchPrompt } from './localResearch';
import { chatPrompt } from './chat';
import {
youtubeSearchResponsePrompt,
youtubeSearchRetrieverPrompt,
@@ -26,7 +27,8 @@ export default {
redditSearchRetrieverPrompt,
wolframAlphaSearchResponsePrompt,
wolframAlphaSearchRetrieverPrompt,
writingAssistantPrompt,
localResearchPrompt,
chatPrompt,
youtubeSearchResponsePrompt,
youtubeSearchRetrieverPrompt,
};

View File

@@ -1,6 +1,6 @@
export const writingAssistantPrompt = `
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Writing Assistant', this means you will be helping the user write a response to a given query.
Since you are a writing assistant, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode.
export const localResearchPrompt = `
You are Perplexica, an AI model who is expert at searching the web and answering user's queries. You are currently set on focus mode 'Local Research', this means you will be helping the user research and interact with local files with citations.
Since you are in local research mode, you would not perform web searches. If you think you lack information to answer the query, you can ask the user for more information or suggest them to switch to a different focus mode.
You will be shared a context that can contain information from files user has uploaded to get answers from. You will have to generate answers upon that.
You have to cite the answer using [number] notation. You must cite the sentences with their relevent context number. You must cite each and every part of the answer so the user can know where the information is coming from.

View File

@@ -6,8 +6,8 @@ export const PROVIDER_INFO = {
key: 'ollama',
displayName: 'Ollama',
};
import { ChatOllama } from '@langchain/community/chat_models/ollama';
import { OllamaEmbeddings } from '@langchain/community/embeddings/ollama';
import { ChatOllama } from '@langchain/ollama';
import { OllamaEmbeddings } from '@langchain/ollama';
export const loadOllamaChatModels = async () => {
const ollamaApiEndpoint = getOllamaApiEndpoint();

View File

@@ -20,15 +20,24 @@ export const searchHandlers: Record<string, MetaSearchAgent> = {
searchWeb: true,
summarizer: false,
}),
writingAssistant: new MetaSearchAgent({
localResearch: new MetaSearchAgent({
activeEngines: [],
queryGeneratorPrompt: '',
responsePrompt: prompts.writingAssistantPrompt,
responsePrompt: prompts.localResearchPrompt,
rerank: true,
rerankThreshold: 0,
searchWeb: false,
summarizer: false,
}),
chat: new MetaSearchAgent({
activeEngines: [],
queryGeneratorPrompt: '',
responsePrompt: prompts.chatPrompt,
rerank: false,
rerankThreshold: 0,
searchWeb: false,
summarizer: false,
}),
wolframAlphaSearch: new MetaSearchAgent({
activeEngines: ['wolframalpha'],
queryGeneratorPrompt: prompts.wolframAlphaSearchRetrieverPrompt,

View File

@@ -434,13 +434,13 @@ class MetaSearchAgent implements MetaSearchAgentType {
private async handleStream(
stream: AsyncGenerator<StreamEvent, any, any>,
emitter: eventEmitter,
llm: BaseChatModel,
) {
for await (const event of stream) {
if (
event.event === 'on_chain_end' &&
event.name === 'FinalSourceRetriever'
) {
``;
emitter.emit(
'data',
JSON.stringify({ type: 'sources', data: event.data.output }),
@@ -459,6 +459,50 @@ class MetaSearchAgent implements MetaSearchAgentType {
event.event === 'on_chain_end' &&
event.name === 'FinalResponseGenerator'
) {
// Get model name safely with better detection
let modelName = 'Unknown';
try {
// @ts-ignore - Different LLM implementations have different properties
if (llm.modelName) {
// @ts-ignore
modelName = llm.modelName;
// @ts-ignore
} else if (llm._llm && llm._llm.modelName) {
// @ts-ignore
modelName = llm._llm.modelName;
// @ts-ignore
} else if (llm.model && llm.model.modelName) {
// @ts-ignore
modelName = llm.model.modelName;
} else if ('model' in llm) {
// @ts-ignore
const model = llm.model;
if (typeof model === 'string') {
modelName = model;
// @ts-ignore
} else if (model && model.modelName) {
// @ts-ignore
modelName = model.modelName;
}
} else if (llm.constructor && llm.constructor.name) {
// Last resort: use the class name
modelName = llm.constructor.name;
}
} catch (e) {
console.error('Failed to get model name:', e);
}
// Send model info before ending
emitter.emit(
'stats',
JSON.stringify({
type: 'modelStats',
data: {
modelName,
},
}),
);
emitter.emit('end');
}
}
@@ -493,7 +537,7 @@ class MetaSearchAgent implements MetaSearchAgentType {
},
);
this.handleStream(stream, emitter);
this.handleStream(stream, emitter, llm);
return emitter;
}

3802
yarn.lock

File diff suppressed because it is too large Load Diff