mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
[app] Combining new GooseResponseForm with URL preview logic
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user