mirror of
https://github.com/PragmaticMachineLearning/probly.git
synced 2026-01-09 13:27:55 -05:00
further refinements
This commit is contained in:
1516
package-lock.json
generated
1516
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,8 @@
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss": "3.4.1",
|
||||
"typescript": "5.3.3",
|
||||
@@ -35,6 +37,7 @@
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-next": "14.1.0"
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ const SpreadsheetApp = () => {
|
||||
response: "",
|
||||
timestamp: new Date(),
|
||||
status: "pending",
|
||||
streaming: true,
|
||||
// If an image was uploaded, store this info in the message
|
||||
hasImage: !!documentImage,
|
||||
documentImage: documentImage
|
||||
@@ -233,14 +232,7 @@ const SpreadsheetApp = () => {
|
||||
}
|
||||
|
||||
if (parsedData.response) {
|
||||
if (parsedData.streaming) {
|
||||
// For streaming content, append to the existing response
|
||||
accumulatedResponse += parsedData.response;
|
||||
} else {
|
||||
// For final content, replace the entire response
|
||||
accumulatedResponse = parsedData.response;
|
||||
}
|
||||
|
||||
accumulatedResponse = parsedData.response;
|
||||
updates = parsedData.updates;
|
||||
chartData = parsedData.chartData;
|
||||
}
|
||||
@@ -255,8 +247,7 @@ const SpreadsheetApp = () => {
|
||||
updates: updates,
|
||||
chartData: chartData,
|
||||
analysis: parsedData?.analysis,
|
||||
streaming: parsedData.streaming ?? false,
|
||||
status: updates || chartData ? "pending" : null,
|
||||
status: updates || chartData ? "completed" : null,
|
||||
}
|
||||
: msg,
|
||||
),
|
||||
@@ -283,8 +274,7 @@ const SpreadsheetApp = () => {
|
||||
updates: updates,
|
||||
chartData: chartData,
|
||||
analysis: lastParsedData?.analysis,
|
||||
streaming: false,
|
||||
status: updates || chartData ? "pending" : null,
|
||||
status: updates || chartData ? "completed" : null,
|
||||
}
|
||||
: msg,
|
||||
),
|
||||
@@ -298,8 +288,8 @@ const SpreadsheetApp = () => {
|
||||
msg.id === newMessage.id
|
||||
? {
|
||||
...msg,
|
||||
response: msg.response + "\n[Generation stopped]",
|
||||
streaming: false,
|
||||
response: msg.response + "\n Response perished in the flames",
|
||||
status: null,
|
||||
}
|
||||
: msg,
|
||||
),
|
||||
@@ -317,7 +307,7 @@ const SpreadsheetApp = () => {
|
||||
? error.message
|
||||
: "An unknown error occurred"
|
||||
}`,
|
||||
streaming: false,
|
||||
status: null,
|
||||
}
|
||||
: msg,
|
||||
),
|
||||
|
||||
@@ -171,7 +171,7 @@ const ChatBox = ({
|
||||
useEffect(() => {
|
||||
if (chatHistory.length > 0) {
|
||||
const lastMessage = chatHistory[chatHistory.length - 1];
|
||||
setIsLoading(!!lastMessage.streaming);
|
||||
setIsLoading(lastMessage.status === "pending");
|
||||
}
|
||||
}, [chatHistory]);
|
||||
|
||||
@@ -217,21 +217,26 @@ const ChatBox = ({
|
||||
}, [promptLibraryOpen, setPromptLibraryOpen, externalIsPromptLibraryOpen]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (message.trim() || isLoading) {
|
||||
if (message.trim() || uploadedDocument || isLoading) {
|
||||
if (isLoading) {
|
||||
onStop();
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const messageToSend = message;
|
||||
const documentToSend = uploadedDocument;
|
||||
|
||||
// Clear message and document preview immediately
|
||||
setMessage("");
|
||||
setUploadedDocument(null);
|
||||
setUploadedDocumentName(null);
|
||||
setIsImageFile(false);
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onSend(messageToSend, uploadedDocument || undefined);
|
||||
// Clear the uploaded document after sending
|
||||
setUploadedDocument(null);
|
||||
await onSend(messageToSend, documentToSend || undefined);
|
||||
} catch (error) {
|
||||
console.error("Error details:", error);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -455,6 +460,7 @@ const ChatBox = ({
|
||||
setUploadedDocument(null);
|
||||
setUploadedDocumentName(null);
|
||||
setIsImageFile(false);
|
||||
console.log("Document cleared:", uploadedDocument);
|
||||
} finally {
|
||||
setUploadingDocument(false);
|
||||
if (fileInputRef.current) {
|
||||
@@ -467,8 +473,22 @@ const ChatBox = ({
|
||||
setUploadedDocument(null);
|
||||
setUploadedDocumentName(null);
|
||||
setIsImageFile(false);
|
||||
console.log("Document cleared:", uploadedDocument);
|
||||
};
|
||||
|
||||
// Add this useEffect to clear document when a new message is added
|
||||
useEffect(() => {
|
||||
if (chatHistory.length > 0) {
|
||||
const lastMessage = chatHistory[chatHistory.length - 1];
|
||||
// If this is a new message (not pending) and it has an image, clear the document
|
||||
if (lastMessage.status !== "pending" && lastMessage.hasImage) {
|
||||
setUploadedDocument(null);
|
||||
setUploadedDocumentName(null);
|
||||
setIsImageFile(false);
|
||||
}
|
||||
}
|
||||
}, [chatHistory]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
||||
{/* Header */}
|
||||
@@ -482,7 +502,7 @@ const ChatBox = ({
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={togglePromptLibrary}
|
||||
className={`p-2 ${promptLibraryOpen ? 'text-blue-500 bg-blue-50' : 'text-gray-500 hover:text-blue-500 hover:bg-blue-50'} rounded-full transition-all duration-200`}
|
||||
className={`p-2 ${promptLibraryOpen ? 'text-[#1A6B4C] bg-[#1A6B4C]/10' : 'text-gray-500 hover:text-[#1A6B4C] hover:bg-[#1A6B4C]/10'} rounded-full transition-all duration-200`}
|
||||
title={promptLibraryOpen ? "Back to chat (Ctrl+Shift+L)" : "Open prompt library (Ctrl+Shift+L)"}
|
||||
>
|
||||
<BookOpen size={18} />
|
||||
@@ -513,7 +533,7 @@ const ChatBox = ({
|
||||
placeholder="Search prompts..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#1A6B4C]"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
@@ -527,16 +547,16 @@ const ChatBox = ({
|
||||
setNewPromptContent('');
|
||||
}
|
||||
}}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors flex items-center gap-2"
|
||||
className="p-2 bg-[#1A6B4C] text-white rounded-full hover:bg-[#1A6B4C]/80 transition-all duration-200 flex items-center justify-center"
|
||||
title={isAddingPrompt && !editingPromptId ? "Cancel" : "Add Prompt"}
|
||||
>
|
||||
{isAddingPrompt && !editingPromptId ? <X size={18} /> : <Plus size={18} />}
|
||||
{isAddingPrompt && !editingPromptId ? 'Cancel' : 'Add Prompt'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Add/Edit Prompt Form */}
|
||||
{isAddingPrompt && (
|
||||
<div className="mt-4 p-4 border border-gray-200 rounded-lg bg-gray-50">
|
||||
<div className="mt-4 p-4 border border-gray-200 rounded-lg bg-[#1A6B4C]/5">
|
||||
<div className="mb-3">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Prompt Title</label>
|
||||
<input
|
||||
@@ -544,7 +564,7 @@ const ChatBox = ({
|
||||
value={newPromptTitle}
|
||||
onChange={(e) => setNewPromptTitle(e.target.value)}
|
||||
placeholder="Enter a descriptive title"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#1A6B4C]"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
@@ -554,25 +574,25 @@ const ChatBox = ({
|
||||
onChange={(e) => setNewPromptContent(e.target.value)}
|
||||
placeholder="Enter your prompt template..."
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#1A6B4C] resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSavePrompt}
|
||||
disabled={!newPromptTitle.trim() || !newPromptContent.trim()}
|
||||
className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors flex items-center gap-2 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||
className="p-2 bg-[#1A6B4C] text-white rounded-full hover:bg-[#1A6B4C]/80 transition-all duration-200 flex items-center justify-center disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||
title={editingPromptId ? "Update Prompt" : "Save Prompt"}
|
||||
>
|
||||
<Save size={18} />
|
||||
{editingPromptId ? 'Update Prompt' : 'Save Prompt'}
|
||||
</button>
|
||||
{editingPromptId && (
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
className="px-4 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors flex items-center gap-2"
|
||||
className="p-2 bg-gray-500 text-white rounded-full hover:bg-gray-600 transition-all duration-200 flex items-center justify-center"
|
||||
title="Cancel"
|
||||
>
|
||||
<X size={18} />
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -591,16 +611,17 @@ const ChatBox = ({
|
||||
{filteredPrompts.map((prompt) => (
|
||||
<div
|
||||
key={prompt.id}
|
||||
className="border border-gray-200 rounded-lg p-4 hover:border-blue-300 hover:bg-blue-50 transition-colors cursor-pointer group"
|
||||
className="border border-gray-200 rounded-lg p-4 hover:border-[#1A6B4C] hover:bg-[#1A6B4C]/5 transition-all duration-200 cursor-pointer group"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<h3 className="font-medium text-gray-800">{prompt.title}</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleSelectPrompt(prompt.content)}
|
||||
className="text-blue-500 hover:text-blue-700 text-sm font-medium"
|
||||
className="p-1.5 text-[#1A6B4C] hover:text-[#1A6B4C]/80 hover:bg-[#1A6B4C]/10 rounded-full transition-all duration-200"
|
||||
title="Use Prompt"
|
||||
>
|
||||
Use
|
||||
<Check size={16} />
|
||||
</button>
|
||||
{!predefinedPrompts.some(p => p.id === prompt.id) && (
|
||||
<>
|
||||
@@ -609,7 +630,8 @@ const ChatBox = ({
|
||||
e.stopPropagation();
|
||||
handleEditPrompt(prompt);
|
||||
}}
|
||||
className="text-gray-500 hover:text-gray-700 transition-colors"
|
||||
className="p-1.5 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-full transition-all duration-200"
|
||||
title="Edit Prompt"
|
||||
>
|
||||
<Edit2 size={16} />
|
||||
</button>
|
||||
@@ -618,7 +640,8 @@ const ChatBox = ({
|
||||
e.stopPropagation();
|
||||
handleDeletePrompt(prompt.id);
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700 transition-colors"
|
||||
className="p-1.5 text-red-500 hover:text-red-700 hover:bg-red-50 rounded-full transition-all duration-200"
|
||||
title="Delete Prompt"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
@@ -649,11 +672,31 @@ const ChatBox = ({
|
||||
<div key={chat.id} className="space-y-3 animate-fadeIn">
|
||||
{/* User Message */}
|
||||
<div className="flex justify-end">
|
||||
<div className="bg-blue-500 text-white rounded-2xl rounded-tr-sm px-4 py-2 max-w-[80%] shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||
<p className="text-sm break-words">{chat.text}</p>
|
||||
<span className="text-xs opacity-75 mt-1 block">
|
||||
{new Date(chat.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
{/* Document Preview above message */}
|
||||
{chat.hasImage && chat.documentImage && (
|
||||
<div className="w-32 h-32 rounded-lg overflow-hidden shadow-md bg-white border border-gray-200">
|
||||
{chat.documentImage.startsWith('data:image/') ? (
|
||||
<img
|
||||
src={chat.documentImage}
|
||||
alt="Document preview"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-gray-50">
|
||||
<FileUp size={24} className="text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Message Bubble */}
|
||||
<div className="bg-[#1A6B4C] text-white rounded-2xl rounded-tr-sm px-4 py-2 max-w-[80%] shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||
<p className="text-sm break-words">{chat.text}</p>
|
||||
<span className="text-xs opacity-75 mt-1 block">
|
||||
{new Date(chat.timestamp).toLocaleTimeString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -661,27 +704,15 @@ const ChatBox = ({
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white rounded-2xl rounded-tl-sm px-4 py-2 max-w-[80%] shadow-sm hover:shadow-md transition-all duration-200 border border-gray-200">
|
||||
<div className="text-sm text-gray-800 break-words">
|
||||
{chat.streaming ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-gray-500">
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
<span className="text-xs">AI is generating response...</span>
|
||||
</div>
|
||||
<div className="border-l-2 border-blue-200 pl-3 font-mono">
|
||||
{chat.response}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ToolResponse
|
||||
response={chat.response}
|
||||
updates={chat.updates}
|
||||
chartData={chat.chartData}
|
||||
analysis={chat.analysis}
|
||||
status={chat.status}
|
||||
onAccept={() => onAccept(chat.updates || [], chat.id)}
|
||||
onReject={() => onReject(chat.id)}
|
||||
/>
|
||||
)}
|
||||
<ToolResponse
|
||||
response={chat.response}
|
||||
updates={chat.updates}
|
||||
chartData={chat.chartData}
|
||||
analysis={chat.analysis}
|
||||
status={chat.status}
|
||||
onAccept={() => onAccept(chat.updates || [], chat.id)}
|
||||
onReject={() => onReject(chat.id)}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-400 mt-1 block">
|
||||
{new Date(chat.timestamp).toLocaleTimeString()}
|
||||
@@ -731,7 +762,7 @@ const ChatBox = ({
|
||||
value={message}
|
||||
onChange={handleTextareaChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={`w-full px-4 py-2 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm resize-none min-h-[80px] bg-white text-gray-800 transition-all duration-200 mb-3 ${
|
||||
className={`w-full px-4 py-2 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#1A6B4C] focus:border-transparent text-sm resize-none min-h-[80px] bg-white text-gray-800 transition-all duration-200 mb-3 ${
|
||||
uploadedDocument ? 'border-transparent' : 'border border-gray-200'
|
||||
}`}
|
||||
placeholder={uploadedDocument ? "What would you like to know about this document?" : "Type your message..."}
|
||||
@@ -745,7 +776,7 @@ const ChatBox = ({
|
||||
<button
|
||||
onClick={handleFileButtonClick}
|
||||
disabled={isLoading || uploadingDocument}
|
||||
className="p-2.5 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white rounded-full transition-all duration-200 disabled:cursor-not-allowed h-8 w-8 flex items-center justify-center shadow-md"
|
||||
className="p-2.5 bg-[#1A6B4C] hover:bg-[#1A6B4C]/80 disabled:bg-gray-300 text-white rounded-full transition-all duration-200 disabled:cursor-not-allowed h-8 w-8 flex items-center justify-center shadow-md"
|
||||
title="Upload document"
|
||||
>
|
||||
{uploadingDocument ? (
|
||||
@@ -760,8 +791,8 @@ const ChatBox = ({
|
||||
{/* Send button on the right */}
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={(!message.trim() && !uploadedDocument) || isLoading}
|
||||
className="p-2.5 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white rounded-full transition-all duration-200 disabled:cursor-not-allowed h-8 w-8 flex items-center justify-center group"
|
||||
disabled={(!message.trim() && !uploadedDocument) && !isLoading}
|
||||
className="p-2.5 bg-[#1A6B4C] hover:bg-[#1A6B4C]/80 disabled:bg-gray-300 text-white rounded-full transition-all duration-200 disabled:cursor-not-allowed h-8 w-8 flex items-center justify-center group"
|
||||
title={isLoading ? "Stop generating" : "Send message"}
|
||||
>
|
||||
{isLoading ? (
|
||||
@@ -772,6 +803,7 @@ const ChatBox = ({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
|
||||
@@ -603,7 +603,7 @@ const Spreadsheet = forwardRef<SpreadsheetRef, SpreadsheetProps>(
|
||||
onChange={handleSheetNameChange}
|
||||
onBlur={handleSheetNameBlur}
|
||||
autoFocus
|
||||
className="w-24 px-1 py-0.5 text-sm border border-blue-400 rounded"
|
||||
className="w-24 px-1 py-0.5 text-sm border border-[#1A6B4C] rounded"
|
||||
onClick={e => e.stopPropagation()}
|
||||
/>
|
||||
</form>
|
||||
@@ -656,7 +656,7 @@ const Spreadsheet = forwardRef<SpreadsheetRef, SpreadsheetProps>(
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
className="flex items-center px-3 py-1.5 text-sm text-blue-600 hover:bg-gray-100 rounded-md mt-1"
|
||||
className="flex items-center px-3 py-1.5 text-sm text-[#1A6B4C] hover:bg-gray-100 rounded-md mt-1"
|
||||
onClick={handleAddSheet}
|
||||
title="Add new sheet"
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BarChart, Check, Table, X } from 'lucide-react';
|
||||
import { BarChart, Check, ChevronDown, ChevronUp, Loader2, Table, X } from 'lucide-react';
|
||||
|
||||
import { CellUpdate } from '@/types/api';
|
||||
import React from 'react';
|
||||
@@ -12,7 +12,7 @@ interface ToolResponseProps {
|
||||
output: string;
|
||||
error?: string;
|
||||
};
|
||||
status: 'pending' | 'accepted' | 'rejected' | null;
|
||||
status: 'pending' | 'accepted' | 'rejected' | 'completed' | null;
|
||||
onAccept: () => void;
|
||||
onReject: () => void;
|
||||
}
|
||||
@@ -35,13 +35,13 @@ const ToolResponse: React.FC<ToolResponseProps> = ({
|
||||
|
||||
// Add state for chart expansion
|
||||
const [chartExpanded, setChartExpanded] = React.useState(false);
|
||||
// Add state for spreadsheet expansion
|
||||
const [spreadsheetExpanded, setSpreadsheetExpanded] = React.useState(false);
|
||||
|
||||
// Function to create a mini spreadsheet visualization
|
||||
const renderMiniSpreadsheet = (updates: CellUpdate[]) => {
|
||||
const renderMiniSpreadsheet = (updates: CellUpdate[], expanded: boolean, setExpanded: (expanded: boolean) => void) => {
|
||||
if (!updates || updates.length === 0) return null;
|
||||
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
|
||||
// Extract column and row information from cell references
|
||||
const cellInfo = updates.map(update => {
|
||||
const match = update.target.match(/([A-Z]+)(\d+)/);
|
||||
@@ -57,6 +57,8 @@ const ToolResponse: React.FC<ToolResponseProps> = ({
|
||||
// Find the range of rows and columns
|
||||
const minRow = Math.min(...cellInfo.map(cell => cell.row));
|
||||
const maxRow = Math.max(...cellInfo.map(cell => cell.row));
|
||||
const minCol = Math.min(...cellInfo.map(cell => cell.col.charCodeAt(0)));
|
||||
const maxCol = Math.max(...cellInfo.map(cell => cell.col.charCodeAt(0)));
|
||||
|
||||
// Get unique columns in alphabetical order
|
||||
const uniqueCols = Array.from(new Set(cellInfo.map(cell => cell.col)))
|
||||
@@ -80,56 +82,81 @@ const ToolResponse: React.FC<ToolResponseProps> = ({
|
||||
updateMap.set(cell.target, cell.formula);
|
||||
});
|
||||
|
||||
// Calculate update range for summary
|
||||
const rangeSummary = `${String.fromCharCode(minCol)}${minRow}:${String.fromCharCode(maxCol)}${maxRow}`;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Update summary */}
|
||||
<div className="text-xs text-gray-600 mb-2">
|
||||
<span className="font-medium">{updates.length}</span> cells updated in range <span className="font-mono">{rangeSummary}</span>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto max-w-full">
|
||||
<table className="text-xs border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-1 bg-gray-100 border border-gray-300"></th>
|
||||
{colsToShow.map(col => (
|
||||
<th key={col} className="p-1 bg-gray-100 border border-gray-300 font-medium text-center w-16">
|
||||
{col}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: displayMaxRow - displayMinRow + 1 }, (_, i) => displayMinRow + i).map(row => (
|
||||
<tr key={row}>
|
||||
<td className="p-1 bg-gray-100 border border-gray-300 font-medium text-center">
|
||||
{row}
|
||||
</td>
|
||||
{colsToShow.map(col => {
|
||||
const cellRef = `${col}${row}`;
|
||||
const hasUpdate = updateMap.has(cellRef);
|
||||
const cellValue = updateMap.get(cellRef) || '';
|
||||
|
||||
return (
|
||||
<td
|
||||
key={cellRef}
|
||||
className={`p-1 border border-gray-300 font-mono text-xs ${hasUpdate ? 'bg-blue-50' : ''}`}
|
||||
title={cellValue}
|
||||
>
|
||||
<div className="truncate max-w-[120px]">
|
||||
{hasUpdate ? cellValue : ''}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<div className={`transition-all duration-300 ease-in-out ${expanded ? 'max-h-[1000px]' : 'max-h-[200px]'} overflow-hidden`}>
|
||||
<table className="text-xs border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-1 bg-gray-100 border border-gray-300"></th>
|
||||
{colsToShow.map(col => (
|
||||
<th key={col} className="p-1 bg-gray-100 border border-gray-300 font-medium text-center w-16">
|
||||
{col}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: displayMaxRow - displayMinRow + 1 }, (_, i) => displayMinRow + i).map(row => (
|
||||
<tr key={row}>
|
||||
<td className="p-1 bg-gray-100 border border-gray-300 font-medium text-center">
|
||||
{row}
|
||||
</td>
|
||||
{colsToShow.map(col => {
|
||||
const cellRef = `${col}${row}`;
|
||||
const hasUpdate = updateMap.has(cellRef);
|
||||
const cellValue = updateMap.get(cellRef) || '';
|
||||
|
||||
return (
|
||||
<td
|
||||
key={cellRef}
|
||||
className={`p-1 border border-gray-300 font-mono text-xs ${hasUpdate ? 'bg-[#1A6B4C]/10' : ''}`}
|
||||
title={cellValue}
|
||||
>
|
||||
<div className="truncate max-w-[120px]">
|
||||
{hasUpdate ? cellValue : ''}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{needsExpand && (
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-blue-600 hover:text-blue-800 transition-colors"
|
||||
>
|
||||
{expanded ? "Show less" : `Show more`}
|
||||
</button>
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="text-xs text-[#1A6B4C] hover:text-[#1A6B4C]/80 transition-colors flex items-center gap-1"
|
||||
>
|
||||
{expanded ? (
|
||||
<>
|
||||
<span>Show less</span>
|
||||
<ChevronUp size={14} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>Show more</span>
|
||||
<ChevronDown size={14} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<span className="text-xs text-gray-500">
|
||||
{expanded ? "Showing all" : `+${maxRow - displayMaxRow} more rows`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -138,35 +165,44 @@ const ToolResponse: React.FC<ToolResponseProps> = ({
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Main response text */}
|
||||
<div className="whitespace-pre-wrap">{mainResponse}</div>
|
||||
<div className="whitespace-pre-wrap">
|
||||
{mainResponse}
|
||||
{status === 'pending' && (
|
||||
<div className="flex items-center gap-2 mt-2 text-[#1A6B4C] text-sm">
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
<span>Generating response...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tool-specific response */}
|
||||
{(hasSpreadsheetUpdates || hasChartData) && (
|
||||
<div className="mt-3 border-t border-gray-200 pt-3">
|
||||
{/* Spreadsheet Updates */}
|
||||
{hasSpreadsheetUpdates && (
|
||||
<div className="bg-blue-50 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center gap-2 text-blue-700 font-medium mb-2">
|
||||
<Table size={16} />
|
||||
<span>Spreadsheet Updates ({updates.length})</span>
|
||||
<div className="bg-[#1A6B4C]/10 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center gap-2 text-[#1A6B4C] font-medium mb-2">
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-auto">
|
||||
{renderMiniSpreadsheet(updates)}
|
||||
{renderMiniSpreadsheet(updates, spreadsheetExpanded, setSpreadsheetExpanded)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chart Data */}
|
||||
{hasChartData && (
|
||||
<div className="bg-purple-50 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center gap-2 text-purple-700 font-medium mb-2">
|
||||
<div className="bg-[#1A6B4C]/10 rounded-lg p-3 mb-3">
|
||||
<div className="flex items-center gap-2 text-[#1A6B4C] font-medium mb-2">
|
||||
<BarChart size={16} />
|
||||
<span>Chart: {chartData.options.title}</span>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<div className="mb-1"><span className="font-medium">Type:</span> {chartData.type}</div>
|
||||
<div className="mb-1"><span className="font-medium">Data:</span> {chartData.options.data.length} rows</div>
|
||||
<div className="bg-purple-100 p-2 rounded max-h-32 overflow-y-auto">
|
||||
<div className="bg-[#1A6B4C]/10 p-2 rounded max-h-32 overflow-y-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -188,7 +224,7 @@ const ToolResponse: React.FC<ToolResponseProps> = ({
|
||||
{chartData.options.data.length > 5 && (
|
||||
<button
|
||||
onClick={() => setChartExpanded(!chartExpanded)}
|
||||
className="text-xs text-purple-600 mt-1 text-center block w-full hover:text-purple-800 transition-colors"
|
||||
className="text-xs text-[#1A6B4C] mt-1 text-center block w-full hover:text-[#1A6B4C]/80 transition-colors"
|
||||
>
|
||||
{chartExpanded ? "Show less" : `+${chartData.options.data.length - 5} more rows`}
|
||||
</button>
|
||||
@@ -199,34 +235,34 @@ const ToolResponse: React.FC<ToolResponseProps> = ({
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
{status === 'pending' && (
|
||||
{(status === 'pending' || status === 'completed') && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
<button
|
||||
onClick={onAccept}
|
||||
className="px-3 py-1.5 bg-green-500 hover:bg-green-600 text-white rounded-full text-xs flex items-center gap-1.5 transition-colors duration-200 group"
|
||||
className="p-1.5 bg-[#1A6B4C] hover:bg-[#1A6B4C]/80 text-white rounded-full flex items-center justify-center transition-all duration-200 group"
|
||||
title="Apply Changes"
|
||||
>
|
||||
<Check size={14} className="group-hover:scale-110 transition-transform duration-200" />
|
||||
Apply Changes
|
||||
<Check size={16} className="group-hover:scale-110 transition-transform duration-200" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onReject}
|
||||
className="px-3 py-1.5 bg-red-500 hover:bg-red-600 text-white rounded-full text-xs flex items-center gap-1.5 transition-colors duration-200 group"
|
||||
className="p-1.5 bg-gray-500 hover:bg-gray-600 text-white rounded-full flex items-center justify-center transition-all duration-200 group"
|
||||
title="Reject Changes"
|
||||
>
|
||||
<X size={14} className="group-hover:scale-110 transition-transform duration-200" />
|
||||
Reject
|
||||
<X size={16} className="group-hover:scale-110 transition-transform duration-200" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Indicators */}
|
||||
{status === 'accepted' && (
|
||||
<div className="mt-2 text-green-500 text-xs flex items-center gap-1 animate-fadeIn">
|
||||
<div className="mt-2 text-[#1A6B4C] text-xs flex items-center gap-1 animate-fadeIn">
|
||||
<Check size={14} />
|
||||
Changes Applied
|
||||
</div>
|
||||
)}
|
||||
{status === 'rejected' && (
|
||||
<div className="mt-2 text-red-500 text-xs flex items-center gap-1 animate-fadeIn">
|
||||
<div className="mt-2 text-[#1A6B4C] text-xs flex items-center gap-1 animate-fadeIn">
|
||||
<X size={14} />
|
||||
Changes Rejected
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
export const SYSTEM_MESSAGE = `You are a spreadsheet automation assistant focused on data operations, visualization, and advanced analysis. Use the available tools strategically based on the complexity of the task.
|
||||
you might be asked to generate/populate the spreadsheet with data, when you're asked to do so, generate synthetic data based on the user query and use the
|
||||
set_spreadsheet_cells function to insert the data into the spreadsheet
|
||||
export const ANALYTICS_SYSTEM_MESSAGE = `You are Probly, a data analyst assistant. Be concise and direct. Guidelines: Get straight to the point - no unnecessary explanations.
|
||||
Provide insights directly. Use precise language. Show formulas without lengthy descriptions. Focus on actionable insights only. No introductions or conclusions.`;
|
||||
|
||||
export const SYSTEM_MESSAGE = `You are Probly, a spreadsheet automation assistant focused on data operations, visualization, and advanced analysis. Be concise and direct.
|
||||
|
||||
COMMUNICATION STYLE:
|
||||
- Get straight to the point - no unnecessary explanations
|
||||
- Provide insights directly without elaborating on process
|
||||
- Use precise, technical language
|
||||
- When suggesting formulas, show them without lengthy descriptions
|
||||
- Focus on actionable insights only
|
||||
- No introductions or conclusions
|
||||
|
||||
Use the available tools strategically based on the complexity of the task.
|
||||
You might be asked to generate/populate the spreadsheet with data, when you're asked to do so, generate synthetic data based on the user query and use the
|
||||
set_spreadsheet_cells function to insert the data into the spreadsheet
|
||||
|
||||
DOCUMENT PROCESSING CAPABILITIES:
|
||||
1. When a user uploads a document, you can analyze it and extract data using the document_analysis tool
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import {
|
||||
convertJsonToTableData,
|
||||
formatSpreadsheetData,
|
||||
generateCellUpdates,
|
||||
structureAnalysisOutput,
|
||||
convertJsonToTableData,
|
||||
|
||||
} from "@/utils/analysisUtils";
|
||||
|
||||
import { OpenAI } from "openai";
|
||||
import { PyodideSandbox } from "@/utils/pyodideSandbox";
|
||||
import { SYSTEM_MESSAGE } from "@/constants/messages";
|
||||
import { analyzeDocumentWithVision } from "@/utils/analysisUtils";
|
||||
import { convertToCSV } from "@/utils/dataUtils";
|
||||
import dotenv from "dotenv";
|
||||
import { tools } from "@/constants/tools";
|
||||
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY || "",
|
||||
});
|
||||
const model = "gpt-4o";
|
||||
const MODEL = "gpt-4o";
|
||||
|
||||
async function handleLLMRequest(
|
||||
message: string,
|
||||
@@ -73,61 +72,24 @@ async function handleLLMRequest(
|
||||
{ role: "user", content: userMessageContent },
|
||||
];
|
||||
|
||||
// First streaming call - always use standard model
|
||||
const stream = await openai.chat.completions.create({
|
||||
// Non-streaming call to OpenAI API
|
||||
const completion = await openai.chat.completions.create({
|
||||
messages: messages as any,
|
||||
model: model,
|
||||
stream: true,
|
||||
model: MODEL,
|
||||
stream: false,
|
||||
});
|
||||
|
||||
let buffer = '';
|
||||
const chunkSize = 100; // Characters
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (aborted) {
|
||||
console.log("Aborting stream processing");
|
||||
await stream.controller.abort();
|
||||
return;
|
||||
}
|
||||
|
||||
const content = chunk.choices[0]?.delta?.content || "";
|
||||
if (content) {
|
||||
buffer += content;
|
||||
|
||||
// Only send to client when buffer reaches a certain size
|
||||
// This reduces the number of network packets
|
||||
if (buffer.length >= chunkSize) {
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
response: buffer,
|
||||
streaming: true,
|
||||
})}\n\n`,
|
||||
);
|
||||
buffer = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send any remaining buffer
|
||||
if (buffer.length > 0) {
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
response: buffer,
|
||||
streaming: true,
|
||||
})}\n\n`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check again before making the tool call
|
||||
if (aborted) return;
|
||||
|
||||
const response = completion.choices[0]?.message?.content || "";
|
||||
|
||||
// Tool completion call - always use standard model
|
||||
const toolCompletion = await openai.chat.completions.create({
|
||||
messages: [
|
||||
...messages,
|
||||
{ role: "assistant", content: buffer },
|
||||
{ role: "assistant", content: response },
|
||||
],
|
||||
model: model,
|
||||
model: MODEL,
|
||||
tools: tools as any,
|
||||
stream: false,
|
||||
});
|
||||
@@ -141,7 +103,7 @@ async function handleLLMRequest(
|
||||
if (toolCalls?.length) {
|
||||
const toolCall = toolCalls[0];
|
||||
let toolData: any = {
|
||||
response: buffer,
|
||||
response: response,
|
||||
};
|
||||
|
||||
if (toolCall.function.name === "set_spreadsheet_cells") {
|
||||
@@ -172,10 +134,7 @@ async function handleLLMRequest(
|
||||
const { analysis_goal, suggested_code, start_cell } = JSON.parse(
|
||||
toolCall.function.arguments,
|
||||
);
|
||||
console.log("SUGGESTED CODE >>>", suggested_code);
|
||||
console.log("START CELL >>>", start_cell);
|
||||
console.log("ANALYSIS GOAL >>>", analysis_goal);
|
||||
|
||||
|
||||
if (aborted) {
|
||||
await sandbox.destroy();
|
||||
return;
|
||||
@@ -191,7 +150,6 @@ async function handleLLMRequest(
|
||||
|
||||
// Structure the output using LLM
|
||||
const structuredOutput = await structureAnalysisOutput(result.stdout, analysis_goal);
|
||||
console.log("STRUCTURED OUTPUT >>>", structuredOutput);
|
||||
|
||||
// Generate cell updates from structured output
|
||||
const generatedUpdates = generateCellUpdates(structuredOutput, start_cell);
|
||||
@@ -275,9 +233,6 @@ async function handleLLMRequest(
|
||||
|
||||
const args = JSON.parse(toolCall.function.arguments);
|
||||
const { operation, target_sheet, start_cell } = args;
|
||||
console.log("OPERATION >>>", operation);
|
||||
console.log("TARGET SHEET >>>", target_sheet);
|
||||
console.log("START CELL >>>", start_cell);
|
||||
|
||||
// We need the document image for analysis
|
||||
if (!documentImage) {
|
||||
@@ -285,25 +240,7 @@ async function handleLLMRequest(
|
||||
} else {
|
||||
try {
|
||||
// Make a second call to the OpenAI Vision API to analyze the document
|
||||
const visionPrompt = `Analyze this document and ${operation.replace('_', ' ')} from it.
|
||||
Format the output as a structured JSON object that can be used to populate a spreadsheet.
|
||||
If extracting a table, create an array of objects with consistent keys.
|
||||
Include metadata about the document if relevant (e.g., date, total amount, etc.).`;
|
||||
|
||||
const visionResponse = await openai.chat.completions.create({
|
||||
model: "gpt-4o",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: visionPrompt },
|
||||
{ type: "image_url", image_url: { url: documentImage } }
|
||||
]
|
||||
}
|
||||
],
|
||||
max_tokens: 1500
|
||||
});
|
||||
|
||||
const visionResponse = await analyzeDocumentWithVision(operation, documentImage);
|
||||
const visionResult = visionResponse.choices[0]?.message?.content || "{}";
|
||||
console.log("VISION RESULT >>>", visionResult);
|
||||
|
||||
@@ -390,7 +327,7 @@ async function handleLLMRequest(
|
||||
} else if (!aborted) {
|
||||
res.write(
|
||||
`data: ${JSON.stringify({
|
||||
response: buffer,
|
||||
response: response,
|
||||
streaming: false,
|
||||
})}\n\n`,
|
||||
);
|
||||
|
||||
@@ -3,9 +3,8 @@ export interface ChatMessage {
|
||||
text: string;
|
||||
response: string;
|
||||
timestamp: Date;
|
||||
status: "pending" | "accepted" | "rejected" | null;
|
||||
status: "pending" | "accepted" | "rejected" | "completed" | null;
|
||||
updates?: CellUpdate[];
|
||||
streaming?: boolean;
|
||||
chartData?: any;
|
||||
analysis?: {
|
||||
goal: string;
|
||||
|
||||
@@ -178,7 +178,7 @@ Guidelines:
|
||||
messages: [
|
||||
{ role: "user", content: prompt }
|
||||
],
|
||||
temperature: 0.2,
|
||||
temperature: 0.1,
|
||||
response_format: { type: "json_object" }
|
||||
});
|
||||
|
||||
@@ -201,4 +201,26 @@ Guidelines:
|
||||
rows: [["Error", `Failed to convert JSON: ${error.message}`]]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const analyzeDocumentWithVision = async (operation: string, documentImage: string, model: string = "gpt-4o") => {
|
||||
const visionPrompt = `Analyze this document and ${operation.replace('_', ' ')} from it.
|
||||
Format the output as a structured JSON object that can be used to populate a spreadsheet.
|
||||
If extracting a table, create an array of objects with consistent keys.
|
||||
Include metadata about the document if relevant (e.g., date, total amount, etc.).`;
|
||||
|
||||
return await openai.chat.completions.create({
|
||||
model: model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: visionPrompt },
|
||||
{ type: "image_url", image_url: { url: documentImage } }
|
||||
]
|
||||
}
|
||||
],
|
||||
temperature: 0.1,
|
||||
max_tokens: 1500
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user