further refinements

This commit is contained in:
tobiadefami
2025-04-05 02:03:49 +01:00
parent bc269882e3
commit 35efdca425
10 changed files with 1768 additions and 224 deletions

1516
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}

View File

@@ -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,
),

View File

@@ -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"

View 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"
>

View File

@@ -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>

View File

@@ -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

View File

@@ -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`,
);

View File

@@ -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;

View File

@@ -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
});
};