diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index e027c1c470..bc7eed288c 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -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' ? ( ) : ( - + )} ))} @@ -247,8 +249,6 @@ export default function ChatWindow() { return (
- - {/* Always render ChatContent but control its visibility */}
@@ -276,9 +276,6 @@ export default function ChatWindow() {
); - - - } /** @@ -307,4 +304,3 @@ async function askAi(promptTemplates: string[]) { return responses; } - diff --git a/ui/desktop/src/components/GooseMessage.tsx b/ui/desktop/src/components/GooseMessage.tsx index a196fddaa3..0c0255292c 100644 --- a/ui/desktop/src/components/GooseMessage.tsx +++ b/ui/desktop/src/components/GooseMessage.tsx @@ -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); - } - }; - - const handleCancel = () => { - if (onInputChange) { - onInputChange({ target: { value: "No thanks" } } as React.ChangeEvent); - } - }; - - const handleSubmit = () => { - if (selectedOption !== null && onInputChange) { - onInputChange({ target: { value: options[selectedOption].optionTitle } } as React.ChangeEvent); - } - }; - - 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 ( -
-
- {message.toolInvocations ? ( -
- {message.toolInvocations.map((toolInvocation) => ( - +
+
+ {message.toolInvocations ? ( +
+ {message.toolInvocations.map((toolInvocation) => ( + + ))} +
+ ) : ( + metadata ? ( + + ) : ( + {message.content} + ) + )} +
+ + {urls.length > 0 && ( +
+ {urls.map((url, index) => ( + ))}
- ) : ( - <> - {(!isOptions || options.length == 0) && ( - {message.content} - )} - {isQuestion && ( -
-
- - -
-
- )} - {isOptions && options.length > 0 && ( -
- {options.map((opt, index) => ( -
handleOptionClick(index)} - className={`p-4 rounded-lg shadow-md cursor-pointer ${ - selectedOption === index - ? 'bg-blue-100 border border-blue-500' - : 'bg-gray-100' - }`} - > -

{opt.optionTitle}

- - {opt.optionDescription} - -
- ))} - -
- )} - )}
diff --git a/ui/desktop/src/components/GooseResponseForm.tsx b/ui/desktop/src/components/GooseResponseForm.tsx new file mode 100644 index 0000000000..aa4c5dc23f --- /dev/null +++ b/ui/desktop/src/components/GooseResponseForm.tsx @@ -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) && ( + {message} + )} + {isQuestion && ( +
+
+ + +
+
+ )} + {isOptions && options.length > 0 && ( +
+ {options.map((opt, index) => ( +
handleOptionClick(index)} + className={`p-4 rounded-lg shadow-md cursor-pointer ${ + selectedOption === index + ? 'bg-blue-100 border border-blue-500' + : 'bg-gray-100' + }`} + > +

{opt.optionTitle}

+ + {opt.optionDescription} + +
+ ))} + +
+ )} + + ); +} diff --git a/ui/desktop/src/components/LinkPreview.tsx b/ui/desktop/src/components/LinkPreview.tsx index a346fa9a3b..d6c9291737 100644 --- a/ui/desktop/src/components/LinkPreview.tsx +++ b/ui/desktop/src/components/LinkPreview.tsx @@ -14,8 +14,6 @@ interface LinkPreviewProps { } async function fetchMetadata(url: string): Promise { - 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 { 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(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) { { - console.log('🔗 Opening URL in Chrome:', url); window.electron.openInChrome(url); }} >