stagent 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/cli.js +146 -2
- package/docs/.coverage-gaps.json +21 -0
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +36 -14
- package/docs/features/chat.md +33 -56
- package/docs/features/cost-usage.md +14 -10
- package/docs/features/dashboard-kanban.md +30 -13
- package/docs/features/delivery-channels.md +198 -0
- package/docs/features/design-system.md +10 -10
- package/docs/features/documents.md +8 -8
- package/docs/features/home-workspace.md +20 -15
- package/docs/features/inbox-notifications.md +22 -10
- package/docs/features/keyboard-navigation.md +11 -11
- package/docs/features/monitoring.md +1 -1
- package/docs/features/playbook.md +30 -32
- package/docs/features/profiles.md +33 -11
- package/docs/features/projects.md +2 -2
- package/docs/features/provider-runtimes.md +58 -14
- package/docs/features/schedules.md +70 -40
- package/docs/features/settings.md +74 -46
- package/docs/features/shared-components.md +7 -15
- package/docs/features/tool-permissions.md +9 -9
- package/docs/features/workflows.md +32 -21
- package/docs/getting-started.md +33 -9
- package/docs/index.md +25 -16
- package/docs/journeys/developer.md +124 -207
- package/docs/journeys/personal-use.md +70 -79
- package/docs/journeys/power-user.md +107 -151
- package/docs/journeys/work-use.md +81 -113
- package/docs/manifest.json +77 -45
- package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
- package/docs/use-cases/agency-operator.md +84 -0
- package/docs/use-cases/solo-founder.md +75 -0
- package/docs/why-stagent.md +59 -0
- package/package.json +10 -3
- package/src/app/api/channels/[id]/route.ts +104 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +116 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +140 -0
- package/src/app/api/channels/inbound/telegram/route.ts +87 -0
- package/src/app/api/channels/route.ts +72 -0
- package/src/app/api/chat/conversations/route.ts +15 -0
- package/src/app/api/chat/entities/search/route.ts +46 -31
- package/src/app/api/data/clear/route.ts +4 -0
- package/src/app/api/data/seed/route.ts +4 -0
- package/src/app/api/documents/route.ts +36 -6
- package/src/app/api/environment/profiles/suggest/route.ts +19 -3
- package/src/app/api/environment/scan/route.ts +8 -1
- package/src/app/api/handoffs/[id]/route.ts +76 -0
- package/src/app/api/handoffs/route.ts +89 -0
- package/src/app/api/memory/route.ts +181 -0
- package/src/app/api/profiles/[id]/route.ts +16 -1
- package/src/app/api/profiles/[id]/test/route.ts +4 -0
- package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
- package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
- package/src/app/api/profiles/assist/route.ts +35 -0
- package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
- package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
- package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
- package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
- package/src/app/api/profiles/import-repo/route.ts +29 -0
- package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
- package/src/app/api/profiles/route.ts +73 -22
- package/src/app/api/runtimes/ollama/route.ts +86 -0
- package/src/app/api/runtimes/suggest/route.ts +29 -0
- package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
- package/src/app/api/schedules/[id]/route.ts +41 -3
- package/src/app/api/schedules/parse/route.ts +66 -0
- package/src/app/api/schedules/route.ts +71 -12
- package/src/app/api/settings/author-default/route.ts +7 -0
- package/src/app/api/settings/learning/route.ts +41 -0
- package/src/app/api/settings/ollama/route.ts +34 -0
- package/src/app/api/settings/providers/route.ts +57 -0
- package/src/app/api/settings/routing/route.ts +24 -0
- package/src/app/api/settings/web-search/route.ts +28 -0
- package/src/app/api/tasks/[id]/execute/route.ts +13 -1
- package/src/app/api/tasks/[id]/respond/route.ts +23 -1
- package/src/app/documents/page.tsx +3 -0
- package/src/app/environment/page.tsx +8 -1
- package/src/app/settings/page.tsx +10 -4
- package/src/app/workflows/[id]/edit/page.tsx +2 -0
- package/src/app/workflows/new/page.tsx +2 -0
- package/src/components/chat/chat-command-popover.tsx +22 -19
- package/src/components/chat/chat-input.tsx +5 -0
- package/src/components/chat/chat-model-selector.tsx +42 -1
- package/src/components/chat/chat-shell.tsx +2 -0
- package/src/components/dashboard/welcome-landing.tsx +9 -9
- package/src/components/environment/artifact-card.tsx +27 -1
- package/src/components/environment/environment-dashboard.tsx +50 -2
- package/src/components/environment/environment-summary-card.tsx +5 -2
- package/src/components/environment/suggested-profiles.tsx +117 -52
- package/src/components/handoffs/handoff-approval-card.tsx +159 -0
- package/src/components/memory/memory-browser.tsx +315 -0
- package/src/components/profiles/learned-context-panel.tsx +4 -4
- package/src/components/profiles/profile-assist-panel.tsx +512 -0
- package/src/components/profiles/profile-browser.tsx +109 -8
- package/src/components/profiles/profile-card.tsx +29 -1
- package/src/components/profiles/profile-detail-view.tsx +200 -28
- package/src/components/profiles/profile-form-view.tsx +220 -82
- package/src/components/profiles/repo-import-wizard.tsx +648 -0
- package/src/components/profiles/smoke-test-editor.tsx +106 -0
- package/src/components/schedules/schedule-create-sheet.tsx +9 -1
- package/src/components/schedules/schedule-form.tsx +348 -9
- package/src/components/schedules/schedule-list.tsx +15 -2
- package/src/components/settings/auth-method-selector.tsx +7 -1
- package/src/components/settings/budget-guardrails-section.tsx +111 -48
- package/src/components/settings/channels-section.tsx +526 -0
- package/src/components/settings/chat-settings-section.tsx +27 -1
- package/src/components/settings/data-management-section.tsx +8 -6
- package/src/components/settings/learning-context-section.tsx +124 -0
- package/src/components/settings/ollama-section.tsx +270 -0
- package/src/components/settings/providers-runtimes-section.tsx +499 -0
- package/src/components/settings/web-search-section.tsx +101 -0
- package/src/components/shared/tag-input.tsx +156 -0
- package/src/components/tasks/kanban-board.tsx +32 -0
- package/src/components/tasks/kanban-column.tsx +4 -2
- package/src/components/tasks/task-card.tsx +1 -0
- package/src/components/tasks/task-chip-bar.tsx +6 -1
- package/src/components/tasks/task-create-panel.tsx +55 -5
- package/src/components/workflows/workflow-form-view.tsx +38 -3
- package/src/hooks/use-chat-autocomplete.ts +24 -26
- package/src/hooks/use-project-skills.ts +66 -0
- package/src/hooks/use-tag-suggestions.ts +31 -0
- package/src/instrumentation.ts +4 -1
- package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
- package/src/lib/agents/agentic-loop.ts +235 -0
- package/src/lib/agents/browser-mcp.ts +59 -4
- package/src/lib/agents/claude-agent.ts +27 -200
- package/src/lib/agents/handoff/bus.ts +164 -0
- package/src/lib/agents/handoff/governance.ts +47 -0
- package/src/lib/agents/handoff/types.ts +16 -0
- package/src/lib/agents/learned-context.ts +27 -7
- package/src/lib/agents/memory/decay.ts +61 -0
- package/src/lib/agents/memory/extractor.ts +181 -0
- package/src/lib/agents/memory/retrieval.ts +96 -0
- package/src/lib/agents/memory/types.ts +6 -0
- package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/project-profiles.ts +193 -0
- package/src/lib/agents/profiles/registry.ts +130 -6
- package/src/lib/agents/profiles/types.ts +28 -0
- package/src/lib/agents/router.ts +174 -2
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
- package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
- package/src/lib/agents/runtime/catalog.ts +57 -2
- package/src/lib/agents/runtime/claude.ts +205 -1
- package/src/lib/agents/runtime/index.ts +22 -0
- package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
- package/src/lib/agents/runtime/openai-direct.ts +514 -0
- package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
- package/src/lib/agents/runtime/types.ts +2 -0
- package/src/lib/agents/tool-permissions.ts +203 -0
- package/src/lib/channels/gateway.ts +321 -0
- package/src/lib/channels/poller.ts +268 -0
- package/src/lib/channels/registry.ts +90 -0
- package/src/lib/channels/slack-adapter.ts +188 -0
- package/src/lib/channels/telegram-adapter.ts +218 -0
- package/src/lib/channels/types.ts +75 -0
- package/src/lib/channels/webhook-adapter.ts +74 -0
- package/src/lib/chat/context-builder.ts +22 -2
- package/src/lib/chat/engine.ts +95 -13
- package/src/lib/chat/ollama-engine.ts +198 -0
- package/src/lib/chat/stagent-tools.ts +106 -20
- package/src/lib/chat/tool-catalog.ts +24 -0
- package/src/lib/chat/tool-registry.ts +90 -0
- package/src/lib/chat/tools/chat-history-tools.ts +4 -4
- package/src/lib/chat/tools/document-tools.ts +7 -7
- package/src/lib/chat/tools/handoff-tools.ts +70 -0
- package/src/lib/chat/tools/notification-tools.ts +4 -4
- package/src/lib/chat/tools/profile-tools.ts +3 -3
- package/src/lib/chat/tools/project-tools.ts +3 -3
- package/src/lib/chat/tools/schedule-tools.ts +29 -13
- package/src/lib/chat/tools/settings-tools.ts +2 -2
- package/src/lib/chat/tools/task-tools.ts +66 -11
- package/src/lib/chat/tools/usage-tools.ts +2 -2
- package/src/lib/chat/tools/workflow-tools.ts +8 -8
- package/src/lib/chat/types.ts +11 -5
- package/src/lib/constants/known-tools.ts +19 -0
- package/src/lib/constants/prose-styles.ts +1 -1
- package/src/lib/constants/settings.ts +7 -0
- package/src/lib/data/channel-bindings.ts +85 -0
- package/src/lib/data/clear.ts +22 -0
- package/src/lib/data/profile-test-results.ts +48 -0
- package/src/lib/data/seed-data/conversations.ts +196 -0
- package/src/lib/data/seed-data/learned-context.ts +99 -0
- package/src/lib/data/seed-data/notifications.ts +54 -1
- package/src/lib/data/seed-data/profile-test-results.ts +96 -0
- package/src/lib/data/seed-data/repo-imports.ts +51 -0
- package/src/lib/data/seed-data/views.ts +60 -0
- package/src/lib/data/seed.ts +51 -0
- package/src/lib/db/bootstrap.ts +162 -0
- package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
- package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
- package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
- package/src/lib/db/schema.ts +190 -1
- package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
- package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
- package/src/lib/environment/auto-scan.ts +48 -0
- package/src/lib/environment/data.ts +25 -0
- package/src/lib/environment/profile-generator.ts +40 -10
- package/src/lib/environment/profile-linker.ts +143 -0
- package/src/lib/environment/profile-rules.ts +96 -0
- package/src/lib/import/dedup.ts +149 -0
- package/src/lib/import/format-adapter.ts +631 -0
- package/src/lib/import/github-api.ts +219 -0
- package/src/lib/import/repo-scanner.ts +251 -0
- package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
- package/src/lib/schedules/active-hours.ts +120 -0
- package/src/lib/schedules/heartbeat-parser.ts +224 -0
- package/src/lib/schedules/heartbeat-prompt.ts +153 -0
- package/src/lib/schedules/nlp-parser.ts +357 -0
- package/src/lib/schedules/scheduler.ts +218 -3
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
- package/src/lib/settings/helpers.ts +6 -0
- package/src/lib/settings/routing.ts +24 -0
- package/src/lib/settings/runtime-setup.ts +28 -1
- package/src/lib/usage/ledger.ts +2 -1
- package/src/lib/validators/__tests__/settings.test.ts +9 -0
- package/src/lib/validators/profile.ts +39 -0
- package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
- package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
- package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
- package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
- package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from "lucide-react";
|
|
22
22
|
import type { LucideIcon } from "lucide-react";
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
getToolCatalogWithSkills,
|
|
25
25
|
groupToolCatalog,
|
|
26
26
|
TOOL_GROUP_ICONS,
|
|
27
27
|
TOOL_GROUP_ORDER,
|
|
@@ -36,6 +36,7 @@ interface ChatCommandPopoverProps {
|
|
|
36
36
|
anchorRect: { top: number; left: number; height: number } | null;
|
|
37
37
|
entityResults: EntitySearchResult[];
|
|
38
38
|
entityLoading: boolean;
|
|
39
|
+
projectProfiles?: Array<{ id: string; name: string; description: string }>;
|
|
39
40
|
onSelect: (item: {
|
|
40
41
|
type: "slash" | "mention";
|
|
41
42
|
id: string;
|
|
@@ -81,6 +82,7 @@ export function ChatCommandPopover({
|
|
|
81
82
|
anchorRect,
|
|
82
83
|
entityResults,
|
|
83
84
|
entityLoading,
|
|
85
|
+
projectProfiles,
|
|
84
86
|
onSelect,
|
|
85
87
|
onClose,
|
|
86
88
|
}: ChatCommandPopoverProps) {
|
|
@@ -116,7 +118,7 @@ export function ChatCommandPopover({
|
|
|
116
118
|
data-chat-autocomplete=""
|
|
117
119
|
className="rounded-lg border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2"
|
|
118
120
|
>
|
|
119
|
-
<Command shouldFilter
|
|
121
|
+
<Command shouldFilter loop>
|
|
120
122
|
{/* Hidden input for cmdk filtering — synced to query */}
|
|
121
123
|
<div className="sr-only">
|
|
122
124
|
<CommandInput value={query} />
|
|
@@ -127,13 +129,17 @@ export function ChatCommandPopover({
|
|
|
127
129
|
{mode === "slash" ? "No matching tools" : "No matching entities"}
|
|
128
130
|
</CommandEmpty>
|
|
129
131
|
|
|
130
|
-
{mode === "slash" &&
|
|
132
|
+
{mode === "slash" && (
|
|
133
|
+
<ToolCatalogItems
|
|
134
|
+
onSelect={onSelect}
|
|
135
|
+
projectProfiles={projectProfiles}
|
|
136
|
+
/>
|
|
137
|
+
)}
|
|
131
138
|
|
|
132
139
|
{mode === "mention" && (
|
|
133
140
|
<MentionItems
|
|
134
141
|
results={entityResults}
|
|
135
142
|
loading={entityLoading}
|
|
136
|
-
query={query}
|
|
137
143
|
onSelect={onSelect}
|
|
138
144
|
/>
|
|
139
145
|
)}
|
|
@@ -147,10 +153,15 @@ export function ChatCommandPopover({
|
|
|
147
153
|
|
|
148
154
|
function ToolCatalogItems({
|
|
149
155
|
onSelect,
|
|
156
|
+
projectProfiles,
|
|
150
157
|
}: {
|
|
151
158
|
onSelect: ChatCommandPopoverProps["onSelect"];
|
|
159
|
+
projectProfiles?: ChatCommandPopoverProps["projectProfiles"];
|
|
152
160
|
}) {
|
|
153
|
-
const catalog =
|
|
161
|
+
const catalog = getToolCatalogWithSkills({
|
|
162
|
+
includeBrowser: true,
|
|
163
|
+
projectProfiles,
|
|
164
|
+
});
|
|
154
165
|
const groups = groupToolCatalog(catalog);
|
|
155
166
|
|
|
156
167
|
return (
|
|
@@ -172,7 +183,9 @@ function ToolCatalogItems({
|
|
|
172
183
|
label: entry.name,
|
|
173
184
|
text: entry.behavior === "execute_immediately"
|
|
174
185
|
? entry.name
|
|
175
|
-
:
|
|
186
|
+
: entry.group === "Skills"
|
|
187
|
+
? `Use the ${entry.name} profile: `
|
|
188
|
+
: `Use ${entry.name} to `,
|
|
176
189
|
})
|
|
177
190
|
}
|
|
178
191
|
>
|
|
@@ -200,27 +213,17 @@ function ToolCatalogItems({
|
|
|
200
213
|
function MentionItems({
|
|
201
214
|
results,
|
|
202
215
|
loading,
|
|
203
|
-
query,
|
|
204
216
|
onSelect,
|
|
205
217
|
}: {
|
|
206
218
|
results: EntitySearchResult[];
|
|
207
219
|
loading: boolean;
|
|
208
|
-
query: string;
|
|
209
220
|
onSelect: ChatCommandPopoverProps["onSelect"];
|
|
210
221
|
}) {
|
|
211
222
|
if (loading && results.length === 0) {
|
|
212
223
|
return (
|
|
213
224
|
<div className="flex items-center gap-2 px-3 py-4 text-sm text-muted-foreground">
|
|
214
225
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
215
|
-
|
|
216
|
-
</div>
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (!query) {
|
|
221
|
-
return (
|
|
222
|
-
<div className="px-3 py-4 text-sm text-muted-foreground text-center">
|
|
223
|
-
Type to search entities...
|
|
226
|
+
Loading...
|
|
224
227
|
</div>
|
|
225
228
|
);
|
|
226
229
|
}
|
|
@@ -228,7 +231,7 @@ function MentionItems({
|
|
|
228
231
|
const grouped = groupByType(results);
|
|
229
232
|
const entityTypes = Object.keys(grouped);
|
|
230
233
|
|
|
231
|
-
if (entityTypes.length === 0
|
|
234
|
+
if (entityTypes.length === 0) {
|
|
232
235
|
return null; // CommandEmpty will show
|
|
233
236
|
}
|
|
234
237
|
|
|
@@ -242,7 +245,7 @@ function MentionItems({
|
|
|
242
245
|
{grouped[type].map((entity) => (
|
|
243
246
|
<CommandItem
|
|
244
247
|
key={`${entity.entityType}-${entity.entityId}`}
|
|
245
|
-
value={`${entity.entityType} ${entity.label}`}
|
|
248
|
+
value={`${entity.entityType} ${entity.label} ${entity.description ?? ""} ${entity.status ?? ""}`}
|
|
246
249
|
onSelect={() =>
|
|
247
250
|
onSelect({
|
|
248
251
|
type: "mention",
|
|
@@ -8,6 +8,7 @@ import { ChatModelSelector } from "./chat-model-selector";
|
|
|
8
8
|
import { ChatCommandPopover } from "./chat-command-popover";
|
|
9
9
|
import { useChatAutocomplete, type MentionReference } from "@/hooks/use-chat-autocomplete";
|
|
10
10
|
import { getToolCatalog } from "@/lib/chat/tool-catalog";
|
|
11
|
+
import { useProjectSkills } from "@/hooks/use-project-skills";
|
|
11
12
|
import type { ChatModelOption } from "@/lib/chat/types";
|
|
12
13
|
|
|
13
14
|
interface ChatInputProps {
|
|
@@ -19,6 +20,7 @@ interface ChatInputProps {
|
|
|
19
20
|
modelId?: string;
|
|
20
21
|
onModelChange?: (modelId: string) => void;
|
|
21
22
|
availableModels?: ChatModelOption[];
|
|
23
|
+
projectId?: string | null;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function ChatInput({
|
|
@@ -30,10 +32,12 @@ export function ChatInput({
|
|
|
30
32
|
modelId,
|
|
31
33
|
onModelChange,
|
|
32
34
|
availableModels,
|
|
35
|
+
projectId,
|
|
33
36
|
}: ChatInputProps) {
|
|
34
37
|
const [value, setValue] = useState("");
|
|
35
38
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
36
39
|
const autocomplete = useChatAutocomplete();
|
|
40
|
+
const { skills: projectSkills } = useProjectSkills(projectId);
|
|
37
41
|
|
|
38
42
|
// Sync textarea ref with autocomplete hook
|
|
39
43
|
useEffect(() => {
|
|
@@ -206,6 +210,7 @@ export function ChatInput({
|
|
|
206
210
|
anchorRect={autocomplete.state.anchorRect}
|
|
207
211
|
entityResults={autocomplete.entityResults}
|
|
208
212
|
entityLoading={autocomplete.entityLoading}
|
|
213
|
+
projectProfiles={projectSkills.length > 0 ? projectSkills : undefined}
|
|
209
214
|
onSelect={handlePopoverSelect}
|
|
210
215
|
onClose={autocomplete.close}
|
|
211
216
|
/>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
3
4
|
import { CHAT_MODELS, DEFAULT_CHAT_MODEL, type ChatModelOption } from "@/lib/chat/types";
|
|
4
5
|
import { Button } from "@/components/ui/button";
|
|
5
6
|
import {
|
|
@@ -24,6 +25,7 @@ const tierEmoji: Record<string, string> = {
|
|
|
24
25
|
Fast: "\u26A1",
|
|
25
26
|
Balanced: "\u2728",
|
|
26
27
|
Best: "\uD83D\uDC8E",
|
|
28
|
+
Local: "\uD83C\uDFE0",
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
export function ChatModelSelector({
|
|
@@ -31,7 +33,27 @@ export function ChatModelSelector({
|
|
|
31
33
|
onModelChange,
|
|
32
34
|
models = CHAT_MODELS,
|
|
33
35
|
}: ChatModelSelectorProps) {
|
|
34
|
-
const
|
|
36
|
+
const [ollamaModels, setOllamaModels] = useState<ChatModelOption[]>([]);
|
|
37
|
+
|
|
38
|
+
// Fetch available Ollama models on mount
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
fetch("/api/runtimes/ollama")
|
|
41
|
+
.then((r) => (r.ok ? r.json() : { models: [] }))
|
|
42
|
+
.then((data: { models?: Array<{ name: string; details?: { parameter_size?: string } }> }) => {
|
|
43
|
+
const models = (data.models ?? []).map((m) => ({
|
|
44
|
+
id: `ollama:${m.name}`,
|
|
45
|
+
label: m.name.replace(/:latest$/, ""),
|
|
46
|
+
provider: "ollama" as const,
|
|
47
|
+
tier: "Local",
|
|
48
|
+
costLabel: "Free",
|
|
49
|
+
}));
|
|
50
|
+
setOllamaModels(models);
|
|
51
|
+
})
|
|
52
|
+
.catch(() => {});
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const allModels = [...models, ...ollamaModels];
|
|
56
|
+
const current = allModels.find((m) => m.id === modelId) ?? allModels[0] ?? { id: modelId, label: modelId, provider: "anthropic" as const, tier: "Balanced", costLabel: "$$" };
|
|
35
57
|
|
|
36
58
|
const anthropicModels = models.filter(
|
|
37
59
|
(m) => m.provider === "anthropic"
|
|
@@ -83,6 +105,25 @@ export function ChatModelSelector({
|
|
|
83
105
|
</DropdownMenuGroup>
|
|
84
106
|
</>
|
|
85
107
|
)}
|
|
108
|
+
|
|
109
|
+
{ollamaModels.length > 0 && (
|
|
110
|
+
<>
|
|
111
|
+
<DropdownMenuSeparator />
|
|
112
|
+
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
|
113
|
+
Ollama (Local)
|
|
114
|
+
</DropdownMenuLabel>
|
|
115
|
+
<DropdownMenuGroup>
|
|
116
|
+
{ollamaModels.map((m) => (
|
|
117
|
+
<ModelMenuItem
|
|
118
|
+
key={m.id}
|
|
119
|
+
model={m}
|
|
120
|
+
isSelected={m.id === modelId}
|
|
121
|
+
onSelect={onModelChange}
|
|
122
|
+
/>
|
|
123
|
+
))}
|
|
124
|
+
</DropdownMenuGroup>
|
|
125
|
+
</>
|
|
126
|
+
)}
|
|
86
127
|
</DropdownMenuContent>
|
|
87
128
|
</DropdownMenu>
|
|
88
129
|
);
|
|
@@ -515,6 +515,7 @@ export function ChatShell({
|
|
|
515
515
|
modelId={modelId}
|
|
516
516
|
onModelChange={handleModelChange}
|
|
517
517
|
availableModels={availableModels}
|
|
518
|
+
projectId={activeConversation?.projectId}
|
|
518
519
|
/>
|
|
519
520
|
</ChatEmptyState>
|
|
520
521
|
</div>
|
|
@@ -539,6 +540,7 @@ export function ChatShell({
|
|
|
539
540
|
modelId={modelId}
|
|
540
541
|
onModelChange={handleModelChange}
|
|
541
542
|
availableModels={availableModels}
|
|
543
|
+
projectId={activeConversation?.projectId}
|
|
542
544
|
/>
|
|
543
545
|
</>
|
|
544
546
|
)}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import { Button } from "@/components/ui/button";
|
|
3
|
-
import { Shield,
|
|
3
|
+
import { Shield, Zap, Wallet } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
const pillars = [
|
|
6
6
|
{
|
|
7
7
|
icon: Shield,
|
|
8
|
-
title: "
|
|
9
|
-
description: "Every agent action
|
|
8
|
+
title: "Your Rules, Enforced",
|
|
9
|
+
description: "Every agent action respects your policies. Full audit trail for every decision.",
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
icon:
|
|
13
|
-
title: "
|
|
14
|
-
description: "Build workflow templates once, run them many times.
|
|
12
|
+
icon: Zap,
|
|
13
|
+
title: "Business on Autopilot",
|
|
14
|
+
description: "Build workflow templates once, run them many times. 21 specialist profiles ready to deploy.",
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
icon: Wallet,
|
|
18
|
-
title: "
|
|
19
|
-
description: "Track spend per task, per
|
|
18
|
+
title: "Know What You Spend",
|
|
19
|
+
description: "Track spend per task, per provider. Budget guardrails prevent surprise bills.",
|
|
20
20
|
},
|
|
21
21
|
];
|
|
22
22
|
|
|
@@ -31,7 +31,7 @@ export function WelcomeLanding() {
|
|
|
31
31
|
Welcome to Stagent
|
|
32
32
|
</h1>
|
|
33
33
|
<p className="text-base text-muted-foreground mb-8 max-w-lg">
|
|
34
|
-
|
|
34
|
+
Your AI Business Operating System. Deploy AI agents, automate business processes, and maintain full control of spend and execution.
|
|
35
35
|
</p>
|
|
36
36
|
|
|
37
37
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 w-full mb-8">
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { Card, CardContent } from "@/components/ui/card";
|
|
4
4
|
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Bot, Link as LinkIcon } from "lucide-react";
|
|
5
7
|
import type { EnvironmentArtifactRow } from "@/lib/db/schema";
|
|
6
8
|
import { CATEGORY_META } from "./summary-cards-row";
|
|
7
9
|
import { PersonaIndicator } from "./persona-indicator";
|
|
@@ -9,6 +11,7 @@ import { PersonaIndicator } from "./persona-indicator";
|
|
|
9
11
|
interface ArtifactCardProps {
|
|
10
12
|
artifact: EnvironmentArtifactRow;
|
|
11
13
|
onClick: () => void;
|
|
14
|
+
onCreateProfile?: () => void;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
function formatSize(bytes: number): string {
|
|
@@ -17,7 +20,7 @@ function formatSize(bytes: number): string {
|
|
|
17
20
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
export function ArtifactCard({ artifact, onClick }: ArtifactCardProps) {
|
|
23
|
+
export function ArtifactCard({ artifact, onClick, onCreateProfile }: ArtifactCardProps) {
|
|
21
24
|
const meta = CATEGORY_META[artifact.category];
|
|
22
25
|
const Icon = meta?.icon;
|
|
23
26
|
|
|
@@ -57,10 +60,33 @@ export function ArtifactCard({ artifact, onClick }: ArtifactCardProps) {
|
|
|
57
60
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
|
58
61
|
{artifact.scope}
|
|
59
62
|
</Badge>
|
|
63
|
+
{/* Profile linkage indicator for skill artifacts */}
|
|
64
|
+
{artifact.category === "skill" && artifact.linkedProfileId && (
|
|
65
|
+
<Badge variant="default" className="text-[10px] px-1.5 py-0 gap-0.5">
|
|
66
|
+
<LinkIcon className="h-2.5 w-2.5" />
|
|
67
|
+
Profile
|
|
68
|
+
</Badge>
|
|
69
|
+
)}
|
|
60
70
|
<span className="text-[10px] text-muted-foreground ml-auto">
|
|
61
71
|
{formatSize(artifact.sizeBytes)}
|
|
62
72
|
</span>
|
|
63
73
|
</div>
|
|
74
|
+
|
|
75
|
+
{/* Create Profile button for unlinked skill artifacts */}
|
|
76
|
+
{artifact.category === "skill" && !artifact.linkedProfileId && onCreateProfile && (
|
|
77
|
+
<Button
|
|
78
|
+
variant="outline"
|
|
79
|
+
size="sm"
|
|
80
|
+
className="w-full text-xs gap-1.5"
|
|
81
|
+
onClick={(e) => {
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
onCreateProfile();
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
<Bot className="h-3 w-3" />
|
|
87
|
+
Create Profile
|
|
88
|
+
</Button>
|
|
89
|
+
)}
|
|
64
90
|
</CardContent>
|
|
65
91
|
</Card>
|
|
66
92
|
);
|
|
@@ -17,7 +17,9 @@ import { CheckpointList } from "./checkpoint-list";
|
|
|
17
17
|
import { TemplateList } from "./template-list";
|
|
18
18
|
import { HealthScoreCard } from "./health-score-card";
|
|
19
19
|
import { SuggestedProfiles } from "./suggested-profiles";
|
|
20
|
+
import { ProfileCreateDialog } from "./profile-create-dialog";
|
|
20
21
|
import type { HealthScore } from "@/lib/environment/health-scoring";
|
|
22
|
+
import type { ProfileSuggestion } from "@/lib/environment/profile-rules";
|
|
21
23
|
import { DiscoverWorkspaceDialog } from "@/components/workspace/discover-workspace-dialog";
|
|
22
24
|
|
|
23
25
|
interface EnvironmentDashboardProps {
|
|
@@ -28,6 +30,7 @@ interface EnvironmentDashboardProps {
|
|
|
28
30
|
checkpoints?: EnvironmentCheckpointRow[];
|
|
29
31
|
templates?: EnvironmentTemplateRow[];
|
|
30
32
|
healthScore?: HealthScore | null;
|
|
33
|
+
scanPath?: string;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export function EnvironmentDashboard({
|
|
@@ -38,6 +41,7 @@ export function EnvironmentDashboard({
|
|
|
38
41
|
checkpoints = [],
|
|
39
42
|
templates = [],
|
|
40
43
|
healthScore,
|
|
44
|
+
scanPath,
|
|
41
45
|
}: EnvironmentDashboardProps) {
|
|
42
46
|
const router = useRouter();
|
|
43
47
|
const [scanning, setScanning] = useState(false);
|
|
@@ -47,16 +51,46 @@ export function EnvironmentDashboard({
|
|
|
47
51
|
const [scopeFilter, setScopeFilter] = useState<string | null>(null);
|
|
48
52
|
const [searchQuery, setSearchQuery] = useState("");
|
|
49
53
|
const [discoverOpen, setDiscoverOpen] = useState(false);
|
|
54
|
+
const [createProfileSuggestion, setCreateProfileSuggestion] = useState<ProfileSuggestion | null>(null);
|
|
55
|
+
|
|
56
|
+
/** Convert an unlinked skill artifact into a quick suggestion for profile creation. */
|
|
57
|
+
function artifactToSuggestion(artifact: EnvironmentArtifactRow): ProfileSuggestion {
|
|
58
|
+
const name = artifact.name
|
|
59
|
+
.replace(/-/g, " ")
|
|
60
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
61
|
+
let description = `Discovered skill: ${artifact.name}`;
|
|
62
|
+
if (artifact.metadata) {
|
|
63
|
+
try {
|
|
64
|
+
const meta = JSON.parse(artifact.metadata);
|
|
65
|
+
if (meta.description) description = meta.description;
|
|
66
|
+
} catch { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ruleId: `discovered-${artifact.name}`,
|
|
70
|
+
name,
|
|
71
|
+
description,
|
|
72
|
+
confidence: 0.5,
|
|
73
|
+
tier: "discovered",
|
|
74
|
+
matchedArtifacts: [{ id: artifact.id, name: artifact.name, category: artifact.category }],
|
|
75
|
+
suggestedTools: ["Read", "Grep", "Glob", "Bash"],
|
|
76
|
+
systemPrompt: description,
|
|
77
|
+
tags: artifact.name.split("-").filter((t) => t.length > 2),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
50
80
|
|
|
51
81
|
const handleScan = useCallback(async () => {
|
|
52
82
|
setScanning(true);
|
|
53
83
|
try {
|
|
54
|
-
await fetch("/api/environment/scan", {
|
|
84
|
+
await fetch("/api/environment/scan", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({ projectDir: scanPath }),
|
|
88
|
+
});
|
|
55
89
|
router.refresh();
|
|
56
90
|
} finally {
|
|
57
91
|
setScanning(false);
|
|
58
92
|
}
|
|
59
|
-
}, [router]);
|
|
93
|
+
}, [router, scanPath]);
|
|
60
94
|
|
|
61
95
|
// Filter artifacts client-side
|
|
62
96
|
const filtered = artifacts.filter((a) => {
|
|
@@ -164,6 +198,11 @@ export function EnvironmentDashboard({
|
|
|
164
198
|
key={artifact.id}
|
|
165
199
|
artifact={artifact}
|
|
166
200
|
onClick={() => setSelectedArtifact(artifact)}
|
|
201
|
+
onCreateProfile={
|
|
202
|
+
artifact.category === "skill" && !artifact.linkedProfileId
|
|
203
|
+
? () => setCreateProfileSuggestion(artifactToSuggestion(artifact))
|
|
204
|
+
: undefined
|
|
205
|
+
}
|
|
167
206
|
/>
|
|
168
207
|
))}
|
|
169
208
|
</div>
|
|
@@ -195,6 +234,15 @@ export function EnvironmentDashboard({
|
|
|
195
234
|
onOpenChange={setDiscoverOpen}
|
|
196
235
|
onComplete={() => router.refresh()}
|
|
197
236
|
/>
|
|
237
|
+
|
|
238
|
+
{/* Profile creation from artifact card's "Create Profile" button */}
|
|
239
|
+
<ProfileCreateDialog
|
|
240
|
+
suggestion={createProfileSuggestion}
|
|
241
|
+
open={!!createProfileSuggestion}
|
|
242
|
+
onOpenChange={(open) => {
|
|
243
|
+
if (!open) setCreateProfileSuggestion(null);
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
198
246
|
</div>
|
|
199
247
|
);
|
|
200
248
|
}
|
|
@@ -36,12 +36,15 @@ export function EnvironmentSummaryCard({
|
|
|
36
36
|
const [scanning, setScanning] = useState(false);
|
|
37
37
|
|
|
38
38
|
useEffect(() => {
|
|
39
|
-
|
|
39
|
+
// Auto-scan on mount: pass workingDirectory so the server can rescan if stale
|
|
40
|
+
const params = new URLSearchParams({ projectId });
|
|
41
|
+
if (workingDirectory) params.set("projectDir", workingDirectory);
|
|
42
|
+
fetch(`/api/environment/scan?${params}`)
|
|
40
43
|
.then((res) => res.json())
|
|
41
44
|
.then((json) => setData(json))
|
|
42
45
|
.catch(() => setData(null))
|
|
43
46
|
.finally(() => setLoading(false));
|
|
44
|
-
}, [projectId]);
|
|
47
|
+
}, [projectId, workingDirectory]);
|
|
45
48
|
|
|
46
49
|
const handleScan = async () => {
|
|
47
50
|
if (!workingDirectory) return;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import { Bot, Sparkles } from "lucide-react";
|
|
4
|
+
import { Bot, Sparkles, ChevronDown, ChevronRight } from "lucide-react";
|
|
6
5
|
import { Card, CardContent } from "@/components/ui/card";
|
|
7
6
|
import { Badge } from "@/components/ui/badge";
|
|
8
7
|
import { Button } from "@/components/ui/button";
|
|
@@ -13,22 +12,91 @@ interface SuggestedProfilesProps {
|
|
|
13
12
|
scanId?: string;
|
|
14
13
|
}
|
|
15
14
|
|
|
15
|
+
function SuggestionCard({
|
|
16
|
+
suggestion,
|
|
17
|
+
onSelect,
|
|
18
|
+
}: {
|
|
19
|
+
suggestion: ProfileSuggestion;
|
|
20
|
+
onSelect: () => void;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<Card className="elevation-1 hover:border-primary/40 transition-colors">
|
|
24
|
+
<CardContent className="p-4 space-y-2.5">
|
|
25
|
+
<div className="flex items-start justify-between">
|
|
26
|
+
<div className="flex items-center gap-2">
|
|
27
|
+
<Bot className="h-4 w-4 text-primary" />
|
|
28
|
+
<span className="text-sm font-medium">{suggestion.name}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
31
|
+
{suggestion.tier === "discovered" && (
|
|
32
|
+
<Badge variant="secondary" className="text-[10px]">
|
|
33
|
+
Discovered
|
|
34
|
+
</Badge>
|
|
35
|
+
)}
|
|
36
|
+
<Badge variant="outline" className="text-[10px]">
|
|
37
|
+
{Math.round(suggestion.confidence * 100)}%
|
|
38
|
+
</Badge>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
43
|
+
{suggestion.description}
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<div className="flex flex-wrap gap-1">
|
|
47
|
+
{suggestion.matchedArtifacts.map((a, i) => (
|
|
48
|
+
<Badge
|
|
49
|
+
key={`${a.id}-${i}`}
|
|
50
|
+
variant="secondary"
|
|
51
|
+
className="text-[10px] px-1.5 py-0"
|
|
52
|
+
>
|
|
53
|
+
{a.name}
|
|
54
|
+
</Badge>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<Button
|
|
59
|
+
variant="outline"
|
|
60
|
+
size="sm"
|
|
61
|
+
className="w-full"
|
|
62
|
+
onClick={onSelect}
|
|
63
|
+
>
|
|
64
|
+
Create Profile
|
|
65
|
+
</Button>
|
|
66
|
+
</CardContent>
|
|
67
|
+
</Card>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
16
71
|
export function SuggestedProfiles({ scanId }: SuggestedProfilesProps) {
|
|
17
|
-
const [
|
|
72
|
+
const [curated, setCurated] = useState<ProfileSuggestion[]>([]);
|
|
73
|
+
const [discovered, setDiscovered] = useState<ProfileSuggestion[]>([]);
|
|
18
74
|
const [loading, setLoading] = useState(true);
|
|
19
|
-
const [selectedSuggestion, setSelectedSuggestion] =
|
|
75
|
+
const [selectedSuggestion, setSelectedSuggestion] =
|
|
76
|
+
useState<ProfileSuggestion | null>(null);
|
|
77
|
+
const [discoveredExpanded, setDiscoveredExpanded] = useState(false);
|
|
20
78
|
|
|
21
79
|
useEffect(() => {
|
|
22
|
-
if (!scanId) {
|
|
80
|
+
if (!scanId) {
|
|
81
|
+
setLoading(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
23
84
|
|
|
24
|
-
fetch(`/api/environment/profiles/suggest?scanId=${scanId}`)
|
|
85
|
+
fetch(`/api/environment/profiles/suggest?scanId=${scanId}&tiered=true`)
|
|
25
86
|
.then((res) => res.json())
|
|
26
|
-
.then((data) =>
|
|
27
|
-
|
|
87
|
+
.then((data) => {
|
|
88
|
+
setCurated(data.curated || []);
|
|
89
|
+
setDiscovered(data.discovered || []);
|
|
90
|
+
})
|
|
91
|
+
.catch(() => {
|
|
92
|
+
setCurated([]);
|
|
93
|
+
setDiscovered([]);
|
|
94
|
+
})
|
|
28
95
|
.finally(() => setLoading(false));
|
|
29
96
|
}, [scanId]);
|
|
30
97
|
|
|
31
|
-
|
|
98
|
+
const totalCount = curated.length + discovered.length;
|
|
99
|
+
if (loading || totalCount === 0) return null;
|
|
32
100
|
|
|
33
101
|
return (
|
|
34
102
|
<div className="space-y-3">
|
|
@@ -36,54 +104,51 @@ export function SuggestedProfiles({ scanId }: SuggestedProfilesProps) {
|
|
|
36
104
|
<Sparkles className="h-4 w-4 text-primary" />
|
|
37
105
|
<h3 className="text-sm font-medium">Suggested Profiles</h3>
|
|
38
106
|
<Badge variant="secondary" className="text-[10px]">
|
|
39
|
-
{
|
|
107
|
+
{totalCount} suggestion{totalCount !== 1 ? "s" : ""}
|
|
40
108
|
</Badge>
|
|
41
109
|
</div>
|
|
42
110
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<Badge
|
|
56
|
-
variant="outline"
|
|
57
|
-
className="text-[10px] shrink-0"
|
|
58
|
-
>
|
|
59
|
-
{Math.round(suggestion.confidence * 100)}%
|
|
60
|
-
</Badge>
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
64
|
-
{suggestion.description}
|
|
65
|
-
</p>
|
|
111
|
+
{/* Tier 1: Curated suggestions */}
|
|
112
|
+
{curated.length > 0 && (
|
|
113
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
114
|
+
{curated.map((suggestion) => (
|
|
115
|
+
<SuggestionCard
|
|
116
|
+
key={suggestion.ruleId}
|
|
117
|
+
suggestion={suggestion}
|
|
118
|
+
onSelect={() => setSelectedSuggestion(suggestion)}
|
|
119
|
+
/>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
66
123
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
124
|
+
{/* Tier 2: Discovered suggestions (collapsible) */}
|
|
125
|
+
{discovered.length > 0 && (
|
|
126
|
+
<div className="space-y-2">
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => setDiscoveredExpanded(!discoveredExpanded)}
|
|
129
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
130
|
+
>
|
|
131
|
+
{discoveredExpanded ? (
|
|
132
|
+
<ChevronDown className="h-3 w-3" />
|
|
133
|
+
) : (
|
|
134
|
+
<ChevronRight className="h-3 w-3" />
|
|
135
|
+
)}
|
|
136
|
+
{discovered.length} discoverable skill{discovered.length !== 1 ? "s" : ""}
|
|
137
|
+
</button>
|
|
74
138
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
139
|
+
{discoveredExpanded && (
|
|
140
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
141
|
+
{discovered.map((suggestion) => (
|
|
142
|
+
<SuggestionCard
|
|
143
|
+
key={suggestion.ruleId}
|
|
144
|
+
suggestion={suggestion}
|
|
145
|
+
onSelect={() => setSelectedSuggestion(suggestion)}
|
|
146
|
+
/>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
87
152
|
|
|
88
153
|
<ProfileCreateDialog
|
|
89
154
|
suggestion={selectedSuggestion}
|