[app] Combining new GooseResponseForm with URL preview logic

This commit is contained in:
Alex Hancock
2024-11-25 11:27:06 -05:00
parent 6e15708f02
commit cb7a2393cb
4 changed files with 185 additions and 148 deletions
+6 -10
View File
@@ -97,9 +97,6 @@ function ChatContent({
onFinish: async (message, options) => {
setStatus('Goose is ready');
const promptTemplates = [
"Take a look at this content, if this looks like it could be asking for a confirmation, return QUESTION. If it looks like it is a list of options or plans to choose from, return OPTIONS, otherwise return READY. \n ### Message Content:\n" + message.content,
"If the content is clearly a list of distinct options or plans of action to choose from, and not just a list of things, but clearly a list of things to choose one from from, take into account the Message Content alone, try to format it in a json array, like this JSON array of objects of the form optionTitle:string, optionDescription:string (markdown).\n If is not a list of options or plans to choose from, then return empty list.\n ### Message Content:\n" + message.content,
@@ -178,7 +175,12 @@ function ChatContent({
{message.role === 'user' ? (
<UserMessage message={message} />
) : (
<GooseMessage message={message} messages={messages} metadata={messageMetadata[message.id]} onInputChange={handleInputChange} />
<GooseMessage
message={message}
messages={messages}
metadata={messageMetadata[message.id]}
append={append}
/>
)}
</div>
))}
@@ -247,8 +249,6 @@ export default function ChatWindow() {
return (
<div className="relative w-screen h-screen overflow-hidden bg-transparent flex flex-col">
{/* Always render ChatContent but control its visibility */}
<div style={{ display: mode === 'expanded' ? 'block' : 'none' }}>
<Routes>
@@ -276,9 +276,6 @@ export default function ChatWindow() {
</div>
</div>
);
}
/**
@@ -307,4 +304,3 @@ async function askAi(promptTemplates: string[]) {
return responses;
}
+45 -131
View File
@@ -1,148 +1,62 @@
import React, { useState } from 'react';
import React from 'react';
import ToolInvocation from './ToolInvocation';
import ReactMarkdown from 'react-markdown';
import { GPSIcon } from './ui/icons';
import LinkPreview from './LinkPreview';
import GooseResponseForm from './GooseResponseForm';
import { extractUrls } from '../utils/urlUtils';
interface GooseMessageProps {
message: any;
metadata: any;
onInputChange?: (value: string) => void;
messages: any[];
metadata?: any;
append: (value: any) => void;
}
export default function GooseMessage({ message, metadata, onInputChange }: GooseMessageProps) {
console.log("GooseMessage", metadata);
export default function GooseMessage({ message, metadata, messages, append }: GooseMessageProps) {
// Find the preceding user message
const messageIndex = messages?.findIndex(msg => msg.id === message.id);
const previousMessage = messageIndex > 0 ? messages[messageIndex - 1] : null;
let isReady = false;
let isQuestion = false;
let isOptions = false;
let options = [];
// Get URLs from previous user message (if it exists)
const previousUrls = previousMessage
? extractUrls(previousMessage.content)
: [];
if (metadata) {
isReady = metadata[0] === "READY";
isQuestion = metadata[0] === "QUESTION";
isOptions = metadata[0] === "OPTIONS";
if (isOptions && metadata[1]) {
try {
let optionsData = metadata[1];
// Remove ```json block if present
if (optionsData.startsWith('```json')) {
optionsData = optionsData.replace(/```json/g, '').replace(/```/g, '');
}
// Parse the options JSON
options = JSON.parse(optionsData);
// Validate the structure of each option
options = options.filter(
(opt) =>
typeof opt.optionTitle === 'string' &&
typeof opt.optionDescription === 'string'
);
} catch (err) {
console.error("Failed to parse options data:", err);
options = [];
}
}
}
const [selectedOption, setSelectedOption] = useState(null);
const handleOptionClick = (index) => {
setSelectedOption(index);
};
const handleAccept = () => {
if (onInputChange) {
onInputChange({ target: { value: "accept" } } as React.ChangeEvent<HTMLInputElement>);
}
};
const handleCancel = () => {
if (onInputChange) {
onInputChange({ target: { value: "No thanks" } } as React.ChangeEvent<HTMLInputElement>);
}
};
const handleSubmit = () => {
if (selectedOption !== null && onInputChange) {
onInputChange({ target: { value: options[selectedOption].optionTitle } } as React.ChangeEvent<HTMLInputElement>);
}
};
if (isQuestion || isOptions) {
window.electron.showNotification({
title: 'Goose has a question for you',
body: `please check with goose to approve the plan of action`,
});
}
// Extract URLs from current message, excluding those from the previous user message
const urls = extractUrls(message.content, previousUrls);
return (
<div className="flex mb-4">
<div className="bg-white w-full text-black rounded-2xl p-4 shadow-md">
{message.toolInvocations ? (
<div className="space-y-4">
{message.toolInvocations.map((toolInvocation) => (
<ToolInvocation
key={toolInvocation.toolCallId}
toolInvocation={toolInvocation}
<div className="flex mb-[16px]">
<div className="flex flex-col">
<div className="bg-goose-bubble text-white rounded-2xl p-4">
{message.toolInvocations ? (
<div className="space-y-4">
{message.toolInvocations.map((toolInvocation) => (
<ToolInvocation
key={toolInvocation.toolCallId}
toolInvocation={toolInvocation}
/>
))}
</div>
) : (
metadata ? (
<GooseResponseForm
message={message.content}
metadata={metadata}
append={append}
/>
) : (
<ReactMarkdown className="prose">{message.content}</ReactMarkdown>
)
)}
</div>
{urls.length > 0 && (
<div className="mt-2">
{urls.map((url, index) => (
<LinkPreview key={index} url={url} />
))}
</div>
) : (
<>
{(!isOptions || options.length == 0) && (
<ReactMarkdown className="prose">{message.content}</ReactMarkdown>
)}
{isQuestion && (
<div className="mt-4 bg-gray-100 p-4 rounded-lg shadow-lg">
<div className="flex space-x-4">
<button
onClick={handleAccept}
className="flex items-center gap-2 bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition"
>
<GPSIcon size={14} />
Accept Plan
</button>
<button
onClick={handleCancel}
className="flex items-center gap-2 bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 transition"
>
<GPSIcon size={14} />
Cancel
</button>
</div>
</div>
)}
{isOptions && options.length > 0 && (
<div className="mt-4 space-y-4">
{options.map((opt, index) => (
<div
key={index}
onClick={() => handleOptionClick(index)}
className={`p-4 rounded-lg shadow-md cursor-pointer ${
selectedOption === index
? 'bg-blue-100 border border-blue-500'
: 'bg-gray-100'
}`}
>
<h3 className="font-semibold text-lg">{opt.optionTitle}</h3>
<ReactMarkdown className="prose">
{opt.optionDescription}
</ReactMarkdown>
</div>
))}
<button
onClick={handleSubmit}
className="flex items-center gap-2 bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition mt-4"
>
<GPSIcon size={14} />
Submit
</button>
</div>
)}
</>
)}
</div>
</div>
@@ -0,0 +1,134 @@
import React, { useState } from 'react';
import { GPSIcon } from './ui/icons';
import ReactMarkdown from 'react-markdown';
interface GooseResponseFormProps {
message: string;
metadata: any;
append: (value: any) => void;
}
export default function GooseResponseForm({ message, metadata, append }: GooseResponseFormProps) {
const [selectedOption, setSelectedOption] = useState(null);
let isReady = false;
let isQuestion = false;
let isOptions = false;
let options = [];
if (metadata) {
isReady = metadata[0] === "READY";
isQuestion = metadata[0] === "QUESTION";
isOptions = metadata[0] === "OPTIONS";
if (isOptions && metadata[1]) {
try {
let optionsData = metadata[1];
if (optionsData.startsWith('```json')) {
optionsData = optionsData.replace(/```json/g, '').replace(/```/g, '');
}
options = JSON.parse(optionsData);
options = options.filter(
(opt) =>
typeof opt.optionTitle === 'string' &&
typeof opt.optionDescription === 'string'
);
} catch (err) {
console.error("Failed to parse options data:", err);
options = [];
}
}
}
const handleOptionClick = (index) => {
setSelectedOption(index);
};
const handleAccept = () => {
const message = {
content: "Yes - execute this plan",
role: "user",
};
append(message);
};
const handleCancel = () => {
const message = {
content: "No - do not execute this plan",
role: "user",
};
append(message);
};
const handleSubmit = () => {
if (selectedOption !== null) {
const message = {
content: `Yes - continue with: ${options[selectedOption].optionTitle}`,
role: "user",
};
append(message);
}
};
if (isQuestion || isOptions) {
window.electron.showNotification({
title: 'Goose has a question for you',
body: `please check with goose to approve the plan of action`,
});
}
return (
<>
{(!isOptions || options.length === 0) && (
<ReactMarkdown className="prose">{message}</ReactMarkdown>
)}
{isQuestion && (
<div className="mt-4 bg-gray-100 p-4 rounded-lg shadow-lg">
<div className="flex space-x-4">
<button
onClick={handleAccept}
className="flex items-center gap-2 bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition"
>
<GPSIcon size={14} />
Take flight with this direction
</button>
<button
onClick={handleCancel}
className="flex items-center gap-2 bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 transition"
>
<GPSIcon size={14} />
Cancel
</button>
</div>
</div>
)}
{isOptions && options.length > 0 && (
<div className="mt-4 space-y-4">
{options.map((opt, index) => (
<div
key={index}
onClick={() => handleOptionClick(index)}
className={`p-4 rounded-lg shadow-md cursor-pointer ${
selectedOption === index
? 'bg-blue-100 border border-blue-500'
: 'bg-gray-100'
}`}
>
<h3 className="font-semibold text-lg">{opt.optionTitle}</h3>
<ReactMarkdown className="prose">
{opt.optionDescription}
</ReactMarkdown>
</div>
))}
<button
onClick={handleSubmit}
className="flex items-center gap-2 bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600 transition mt-4"
>
<GPSIcon size={14} />
Submit
</button>
</div>
)}
</>
);
}
@@ -14,8 +14,6 @@ interface LinkPreviewProps {
}
async function fetchMetadata(url: string): Promise<Metadata> {
console.log('🔄 Fetching metadata for URL:', url);
try {
// Fetch the HTML content using the main process
const html = await window.electron.fetchMetadata(url);
@@ -54,8 +52,6 @@ async function fetchMetadata(url: string): Promise<Metadata> {
image = new URL(image, baseUrl).toString();
}
console.log('✨ Extracted metadata:', { title, description, favicon, image });
return {
title: title || url,
description,
@@ -81,14 +77,12 @@ export default function LinkPreview({ url }: LinkPreviewProps) {
const [error, setError] = useState<string | null>(null);
useEffect(() => {
console.log('🔄 LinkPreview mounting for URL:', url);
let mounted = true;
const fetchData = async () => {
try {
const data = await fetchMetadata(url);
if (mounted) {
console.log('✨ Received metadata:', data);
setMetadata(data);
}
} catch (error) {
@@ -123,7 +117,6 @@ export default function LinkPreview({ url }: LinkPreviewProps) {
<Card
className="max-w-[300px] truncate flex items-center bg-link-preview p-3 transition-colors cursor-pointer"
onClick={() => {
console.log('🔗 Opening URL in Chrome:', url);
window.electron.openInChrome(url);
}}
>