diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index 6cc2c894..d8c69bc3 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -67,6 +67,7 @@ "@hey-api/openapi-ts": "^0.64.4", "@modelcontextprotocol/sdk": "^1.8.0", "@playwright/test": "^1.51.1", + "@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/typography": "^0.5.15", "@types/cors": "^2.8.17", "@types/electron": "^1.4.38", @@ -4585,6 +4586,16 @@ "node": ">=10" } }, + "node_modules/@tailwindcss/line-clamp": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz", + "integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index c74a1f93..339fdfe3 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -47,6 +47,7 @@ "@hey-api/openapi-ts": "^0.64.4", "@modelcontextprotocol/sdk": "^1.8.0", "@playwright/test": "^1.51.1", + "@tailwindcss/line-clamp": "^0.4.4", "@tailwindcss/typography": "^0.5.15", "@types/cors": "^2.8.17", "@types/electron": "^1.4.38", diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 91392ce0..78cd5ed5 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -527,23 +527,7 @@ export default function App() { )} {view === 'recipeEditor' && ( setView('chat')} - setView={setView} - onSave={(config) => { - console.log('Saving recipe config:', config); - window.electron.createChatWindow( - undefined, - undefined, - undefined, - undefined, - config, - 'recipeEditor', - { config } - ); - setView('chat'); - }} /> )} {view === 'permission' && ( diff --git a/ui/desktop/src/components/RecipeActivityEditor.tsx b/ui/desktop/src/components/RecipeActivityEditor.tsx new file mode 100644 index 00000000..2ec7c1fd --- /dev/null +++ b/ui/desktop/src/components/RecipeActivityEditor.tsx @@ -0,0 +1,66 @@ +import { useState } from 'react'; + +export default function RecipeActivityEditor({ + activities, + setActivities, +}: { + activities: string[]; + setActivities: (prev: string[]) => void; +}) { + const [newActivity, setNewActivity] = useState(''); + const handleAddActivity = () => { + if (newActivity.trim()) { + setActivities([...activities, newActivity.trim()]); + setNewActivity(''); + } + }; + + const handleRemoveActivity = (activity: string) => { + setActivities(activities.filter((a) => a !== activity)); + }; + return ( +
+ +

+ The top-line prompts and activities that will display within your goose home page. +

+
+
+ {activities.map((activity, index) => ( +
100 ? activity : undefined} + > + {activity.length > 100 ? activity.slice(0, 100) + '...' : activity} + +
+ ))} +
+
+ setNewActivity(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleAddActivity()} + className="flex-1 px-4 py-3 border rounded-lg bg-bgApp text-textStandard placeholder-textPlaceholder focus:outline-none focus:ring-2 focus:ring-borderProminent" + placeholder="Add new activity..." + /> + +
+
+
+ ); +} diff --git a/ui/desktop/src/components/RecipeEditor.tsx b/ui/desktop/src/components/RecipeEditor.tsx index db95533e..efbb85d2 100644 --- a/ui/desktop/src/components/RecipeEditor.tsx +++ b/ui/desktop/src/components/RecipeEditor.tsx @@ -1,15 +1,15 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Recipe } from '../recipe'; import { Buffer } from 'buffer'; import { FullExtensionConfig } from '../extensions'; -import { ChevronRight } from './icons/ChevronRight'; -import Back from './icons/Back'; -import { Bars } from './icons/Bars'; import { Geese } from './icons/Geese'; import Copy from './icons/Copy'; import { Check } from 'lucide-react'; import { useConfig } from './ConfigContext'; import { FixedExtensionEntry } from './ConfigContext'; +import RecipeActivityEditor from './RecipeActivityEditor'; +import RecipeInfoModal from './RecipeInfoModal'; +import RecipeExpandableInfo from './RecipeExpandableInfo'; // import ExtensionList from './settings_v2/extensions/subcomponents/ExtensionList'; interface RecipeEditorProps { @@ -28,10 +28,17 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { const [title, setTitle] = useState(config?.title || ''); const [description, setDescription] = useState(config?.description || ''); const [instructions, setInstructions] = useState(config?.instructions || ''); + const [prompt, setPrompt] = useState(config?.prompt || ''); const [activities, setActivities] = useState(config?.activities || []); const [extensionOptions, setExtensionOptions] = useState([]); const [extensionsLoaded, setExtensionsLoaded] = useState(false); const [copied, setCopied] = useState(false); + const [isRecipeInfoModalOpen, setRecipeInfoModalOpen] = useState(false); + const [recipeInfoModelProps, setRecipeInfoModelProps] = useState<{ + label: string; + value: string; + setValue: (value: string) => void; + } | null>(null); // Initialize selected extensions for the recipe from config or localStorage const [recipeExtensions] = useState(() => { @@ -50,12 +57,10 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { const exts = []; return exts; }); - const [newActivity, setNewActivity] = useState(''); - // Section visibility state - const [activeSection, setActiveSection] = useState< - 'none' | 'activities' | 'instructions' | 'extensions' - >('none'); + const [activeSection, _] = useState<'none' | 'activities' | 'instructions' | 'extensions'>( + 'none' + ); // Load extensions when component mounts and when switching to extensions section useEffect(() => { @@ -95,32 +100,6 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [recipeExtensions, extensionsLoaded]); - // const handleExtensionToggle = (extension: FixedExtensionEntry) => { - // console.log('Toggling extension:', extension.name); - // setRecipeExtensions((prev) => { - // const isSelected = prev.includes(extension.name); - // const newState = isSelected - // ? prev.filter((extName) => extName !== extension.name) - // : [...prev, extension.name]; - - // // Persist to localStorage - // localStorage.setItem('recipe_editor_extensions', JSON.stringify(newState)); - - // return newState; - // }); - // }; - - const handleAddActivity = () => { - if (newActivity.trim()) { - setActivities((prev) => [...prev, newActivity.trim()]); - setNewActivity(''); - } - }; - - const handleRemoveActivity = (activity: string) => { - setActivities((prev) => prev.filter((a) => a !== activity)); - }; - const getCurrentConfig = (): Recipe => { console.log('Creating config with:', { selectedExtensions: recipeExtensions, @@ -134,6 +113,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { description, instructions, activities, + prompt, extensions: recipeExtensions .map((name) => { const extension = extensionOptions.find((e) => e.name === name); @@ -154,16 +134,27 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { return config; }; - const [errors, setErrors] = useState<{ title?: string; description?: string }>({}); + const [errors, setErrors] = useState<{ + title?: string; + description?: string; + instructions?: string; + }>({}); + + const requiredFieldsAreFilled = () => { + return title.trim() && description.trim() && instructions.trim(); + }; const validateForm = () => { - const newErrors: { title?: string; description?: string } = {}; + const newErrors: { title?: string; description?: string; instructions?: string } = {}; if (!title.trim()) { newErrors.title = 'Title is required'; } if (!description.trim()) { newErrors.description = 'Description is required'; } + if (!instructions.trim()) { + newErrors.instructions = 'Instructions are required'; + } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -182,268 +173,180 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { }); }; + const onClickEditTextArea = ({ + label, + value, + setValue, + }: { + label: string; + value: string; + setValue: (value: string) => void; + }) => { + setRecipeInfoModalOpen(true); + setRecipeInfoModelProps({ + label, + value, + setValue, + }); + }; // Reset extensionsLoaded when section changes away from extensions useEffect(() => { if (activeSection !== 'extensions') { setExtensionsLoaded(false); } }, [activeSection]); - - // Render expanded section content - const renderSectionContent = () => { - switch (activeSection) { - case 'activities': - return ( -
- -
- -
-
-

Activities

-

- The top-line prompts and activities that will display within your goose home page. -

-
-
-
- {activities.map((activity, index) => ( -
100 ? activity : undefined} - > - {activity.length > 100 ? activity.slice(0, 100) + '...' : activity} - -
- ))} -
-
- setNewActivity(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleAddActivity()} - className="flex-1 px-4 py-3 bg-bgSubtle text-textStandard rounded-xl placeholder-textPlaceholder focus:outline-none focus:ring-2 focus:ring-borderProminent" - placeholder="Add new activity..." - /> - -
-
-
- ); - - case 'instructions': - return ( -
- -
- -
-
-

Instructions

-

- Hidden instructions that will be passed to the provider to help direct and add - context to your responses. -

-
-