/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * `IdeasPanel` — surface mined patterns as candidate one-click tools. * * The pattern miner runs on idle, scans the local action log, and emits * recurring intent sequences. This panel lists them with a per-pattern * "Author it" affordance: clicking turns the pattern into an * `AuthoringPlan` stub via `host.acceptSuggestion()`, then shows the * `PlanCard` so the user can prune / approve before chat routes it * through the bundle synthesis pipeline. * * Privacy: everything here is local. Patterns are derived from * content-free action metadata only. * * Spec: docs/architecture/ai-customization/06-self-improvement.md §3. */ import { useEffect, useState } from 'react'; import { ArrowRight, Lightbulb, MessageSquarePlus, Sparkles, Wrench } from 'lucide-react'; import { STARTER_IDEAS, type AuthoringPlan, type MinedPattern, type MineEvent, type StarterIdea, } from '@ifc-lite/extensions'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useExtensionHost } from '@/sdk/ExtensionHostProvider'; import { useViewerStore } from '@/store'; import { PlanCard } from './PlanCard'; import { toast } from '@/components/ui/toast'; import { HelpHint } from './HelpHint'; interface IdeasPanelProps { /** Optional override for the approve action. Defaults to seeding the chat panel. */ onApprovePlan?: (plan: AuthoringPlan) => void; } export function IdeasPanel({ onApprovePlan }: IdeasPanelProps) { const host = useExtensionHost(); const queueChatPrompt = useViewerStore((s) => s.queueChatPrompt); const setChatPanelVisible = useViewerStore((s) => s.setChatPanelVisible); const setScriptPanelVisible = useViewerStore((s) => s.setScriptPanelVisible); const setScriptEditorContent = useViewerStore((s) => s.setScriptEditorContent); /** Deep-link from Command Palette → "Author from scratch". */ const ideasOpenEmptyPlan = useViewerStore((s) => s.ideasOpenEmptyPlan); const setIdeasOpenEmptyPlan = useViewerStore((s) => s.setIdeasOpenEmptyPlan); const [event, setEvent] = useState(() => host.getSuggestions()); const [draft, setDraft] = useState(); useEffect(() => { return host.onSuggestions((e) => setEvent(e)); }, [host]); // Honour a deep-link request to open the empty-plan flow. The // flag is one-shot — clear it once we've opened the draft so a // tab switch doesn't reopen it. useEffect(() => { if (ideasOpenEmptyPlan) { handleAuthorFromScratch(); setIdeasOpenEmptyPlan(false); } // handleAuthorFromScratch is defined below in this component and // stable across renders (no deps), so referencing it here is safe. // eslint-disable-next-line react-hooks/exhaustive-deps }, [ideasOpenEmptyPlan]); const patterns = event?.patterns ?? []; /** * Starter "Try it" routes directly to chat with the plan baked into * the prompt AND opens the Script Editor so the user sees where * generated code will land. We only seed the editor with a * placeholder when it's empty / on the default boilerplate — * clobbering a user's in-progress code would be hostile. */ const handleAcceptStarter = (idea: StarterIdea) => { const prompt = buildAuthoringPrompt(idea.plan); queueChatPrompt(prompt); setChatPanelVisible(true); setScriptPanelVisible(true); const current = useViewerStore.getState().scriptEditorContent ?? ''; const isPristine = current.trim().length === 0 || /Write your BIM script here/.test(current); if (isPristine) { setScriptEditorContent( `// Authoring: ${idea.plan.summary}\n` + `//\n` + `// The AI is reading the plan in the chat panel.\n` + `// Answer the follow-ups and the generated handler will land here.\n`, ); } toast.success(`Sent "${idea.plan.summary}" to chat — answer follow-ups to refine.`); }; /** Power-user path: open the PlanCard with the starter pre-filled. */ const handleCustomizeStarter = (idea: StarterIdea) => { setDraft({ ...idea.plan }); }; /** Mined-pattern accept — always uses the PlanCard since the * generated plan is rougher and benefits from review. */ const handleAcceptMined = (pattern: MinedPattern) => { setDraft(host.acceptSuggestion(pattern)); }; /** * Open the Plan Card with an empty plan. The user describes the * extension in the plan fields before approval kicks off chat — * plan-before-code without needing a mined pattern or a starter. */ const handleAuthorFromScratch = () => { setDraft({ summary: '', rationale: '', contributions: [], capabilities: [], triggers: [], widgets: [], tests: [], }); }; const handleApprove = (plan: AuthoringPlan) => { setDraft(undefined); if (onApprovePlan) { onApprovePlan(plan); return; } // Default routing: open chat AND the script editor, then seed // chat with a prompt describing the approved plan. The script // panel is where the generated code lands — opening both keeps // this consistent with the "Try it" flow. queueChatPrompt(buildAuthoringPrompt(plan)); setChatPanelVisible(true); setScriptPanelVisible(true); toast.success(`Routing "${plan.summary}" to the AI assistant…`); }; if (draft) { return (
setDraft(undefined)} />
); } return (

Ideas

{event && ( {patterns.length} {patterns.length === 1 ? 'suggestion' : 'suggestions'} {' · '} {event.eventCount} events )}

Curated starter ideas show what one-click tools you can build today.

Recurring suggestions appear once a workflow shows up repeatedly in your local activity log (model loads, lens applies, exports). Thresholds relax while the log is sparse so something appears early; tightens as data accumulates.

Click Try it to send the idea to the AI chat assistant — chat opens and you answer follow-ups. Click Customize plan first… if you want to prune capabilities or rename the command before chat sees it.

The action log is local. Nothing here leaves your device.

Recurring sequences in your local activity log. Nothing here leaves your device.
{/* Recurring (mined) patterns. Tightens as the log grows. */} {patterns.length > 0 && (
Recurring in your activity
    {patterns.map((pattern, i) => (
  • ')}:${i}`} className="px-4 py-3">
    {pattern.sequence.map((intent, idx) => ( {intent} {idx < pattern.sequence.length - 1 && ( )} ))}
    {pattern.occurrences}× across {pattern.sessionsTouched}{' '} {pattern.sessionsTouched === 1 ? 'session' : 'sessions'} {' · last '} {new Date(pattern.lastSeenAt).toLocaleString()} {' · score '} {pattern.score.toFixed(2)}
  • ))}
)} {/* Always-on starter ideas. Hand-curated IFC/AEC workflows the user can author without waiting for the miner to learn from their activity. Tagged "Example" so they don't masquerade as personalised suggestions. */}
{patterns.length === 0 ? 'Try one of these to get started' : 'Examples — common AEC tools'}
    {STARTER_IDEAS.map((idea) => (
  • {idea.icon} {idea.plan.summary} {idea.category}

    {idea.plan.rationale}

  • ))}
{/* "Author from scratch" CTA — opens the Plan Card with an empty plan so the user can describe whatever they want before chat takes over. */}

Open an empty plan. Fill in what you want, then approve → chat AI assembles the bundle.

); } function buildAuthoringPrompt(plan: AuthoringPlan): string { const contributions = plan.contributions .map((c) => `- ${c.kind}: ${c.label}${c.slot ? ` (slot: ${c.slot})` : ''}`) .join('\n'); const caps = plan.capabilities.map((c) => `\`${c}\``).join(', ') || '(none)'; return [ `Author an extension for me: ${plan.summary}`, '', `Rationale: ${plan.rationale}`, '', `Contributions:\n${contributions || '- (to be designed)'}`, '', `Capabilities requested: ${caps}`, `Triggers: ${plan.triggers.join(', ') || '(to be designed)'}`, plan.notes ? `\nNotes: ${plan.notes}` : '', ].filter(Boolean).join('\n'); }