@@ -33,11 +46,14 @@ export function CustomizePanel({ fields, params, onParamChange, onReset }: Custo
Reset
+ {autofill}
{sortedGroups.map(([groupName, groupFields]) => (
{groupName}
- {groupFields.map((field) => renderField(field, params[field.key] ?? field.defaultValue, onParamChange))}
+ {groupFields.map((field) =>
+ renderField(field, params[field.key] ?? field.defaultValue, onParamChange, category, currentValues)
+ )}
))}
@@ -45,7 +61,13 @@ export function CustomizePanel({ fields, params, onParamChange, onReset }: Custo
);
}
-function renderField(field: TemplateField, value: any, onChange: (key: string, value: any) => void) {
+function renderField(
+ field: TemplateField,
+ value: any,
+ onChange: (key: string, value: any) => void,
+ category?: TemplateCategory,
+ currentValues?: Record
+) {
switch (field.type) {
case 'text':
return (
@@ -56,6 +78,14 @@ function renderField(field: TemplateField, value: any, onChange: (key: string, v
onChange={(v) => onChange(field.key, v)}
placeholder={field.placeholder}
required={field.required}
+ action={
+ onChange(field.key, v)}
+ />
+ }
/>
);
case 'textarea':
@@ -68,6 +98,14 @@ function renderField(field: TemplateField, value: any, onChange: (key: string, v
placeholder={field.placeholder}
required={field.required}
multiline
+ action={
+ onChange(field.key, v)}
+ />
+ }
/>
);
case 'color':
diff --git a/src/components/editor/EditorApp.tsx b/src/components/editor/EditorApp.tsx
index b992b98..c7708e8 100644
--- a/src/components/editor/EditorApp.tsx
+++ b/src/components/editor/EditorApp.tsx
@@ -10,12 +10,45 @@ import { CustomizePanel } from './CustomizePanel';
import { ExportBar } from './ExportBar';
import { PlatformPreviewStrip } from './PlatformPreviewStrip';
import { MobileEditorTabs } from './MobileEditorTabs';
+import { AIProvider, useAI } from './AIContext';
+import { AISettingsModal } from './AISettingsModal';
+import { AIAutofill } from './AIAutofill';
interface EditorAppProps {
initialCategory?: TemplateCategory;
}
export function EditorApp({ initialCategory }: EditorAppProps) {
+ return (
+
+
+
+ );
+}
+
+function AITopbarButton() {
+ const { isConfigured, openSettings } = useAI();
+ return (
+
+ );
+}
+
+function AISettingsWrapper() {
+ const { settingsOpen, closeSettings } = useAI();
+ return ;
+}
+
+function EditorAppInner({ initialCategory }: EditorAppProps) {
// Import templates directly — functions can't cross the Astro→React serialization boundary
const templates = useMemo(() => getAllTemplates(), []);
@@ -26,7 +59,7 @@ export function EditorApp({ initialCategory }: EditorAppProps) {
return templates[0];
}, [templates, initialCategory]);
- const { state, setTemplate, setParam, resetDefaults, setCategory, setSearch, apiUrl, downloadUrl } =
+ const { state, setTemplate, setParam, setParams, resetDefaults, setCategory, setSearch, apiUrl, downloadUrl } =
useEditorState(defaultTemplate);
const { svg, loading, error, render } = useSatoriRenderer();
@@ -83,6 +116,29 @@ export function EditorApp({ initialCategory }: EditorAppProps) {
}
}, [currentTemplate, resetDefaults]);
+ const handleAutofill = useCallback(
+ (result: { templateId: string; fields: Record; colors?: Record }) => {
+ // Switch template
+ const template = getTemplate(result.templateId);
+ if (template) {
+ setTemplate(template);
+ // Apply autofilled fields after template switch
+ setTimeout(() => {
+ const merged = { ...result.fields };
+ if (result.colors) Object.assign(merged, result.colors);
+ setParams(merged);
+ }, 0);
+ } else {
+ // If template not found, just apply fields to current template
+ const merged = { ...result.fields };
+ if (result.colors) Object.assign(merged, result.colors);
+ setParams(merged);
+ }
+ if (isMobile) setMobileTab('customize');
+ },
+ [setTemplate, setParams, isMobile]
+ );
+
return (
{/* Top Bar */}
@@ -94,6 +150,7 @@ export function EditorApp({ initialCategory }: EditorAppProps) {
Template:
{currentTemplate.name}
+
@@ -144,15 +201,22 @@ export function EditorApp({ initialCategory }: EditorAppProps) {
/>
) : (
+ <>