From 9ca51d639ebfffbe2197dc3764705e9beb681827 Mon Sep 17 00:00:00 2001 From: ngav1491 Date: Sun, 23 Aug 2026 14:59:16 +0700 Subject: [PATCH] feat: seed 10 builtin skills with idempotent sync engine Add EnsureBuiltinSeeds() to the skill service and wire it at app startup so every deployment ships a usable set of built-in skills out of the box. Seed engine (backend/internal/application/skill/seed.go): - Embeds seeddata/*.md (frontmatter: name/title/description/sort) - Validates limits: title <= 64 chars, description <= 256, body <= 10,000 runes - Idempotent on restart: - missing record -> create - untouched record (created_by == 0 && updated_by == 0) -> re-sync content on upgrade - admin-modified record -> never overwritten - Wired in backend/internal/app/app.go right after skill service init Bundled skills (10): diagram-svg, mermaid-diagram, html-artifact, svg-icon, chart-svg, report-writing, polish-writing, translate, code-review, slide-outline - authored for DEEIX render stack (SVG block preview, native mermaid, sandboxed HTML artifacts). Tests: go test ./internal/application/skill/... PASS --- backend/internal/app/app.go | 3 + backend/internal/application/auth/service.go | 2 +- backend/internal/application/skill/seed.go | 217 ++++ .../internal/application/skill/seed_test.go | 227 +++++ .../application/skill/seeddata/chart-svg.md | 47 + .../application/skill/seeddata/code-review.md | 49 + .../application/skill/seeddata/diagram-svg.md | 54 + .../skill/seeddata/html-artifact.md | 45 + .../skill/seeddata/mermaid-diagram.md | 46 + .../skill/seeddata/polish-writing.md | 46 + .../skill/seeddata/report-writing.md | 46 + .../skill/seeddata/slide-outline.md | 55 + .../application/skill/seeddata/svg-icon.md | 45 + .../application/skill/seeddata/translate.md | 43 + backend/internal/infra/llm/openai_images.go | 15 +- .../internal/infra/llm/openai_images_test.go | 14 +- .../components/admin-date-range-filter.tsx | 4 +- .../components/admin-date-time-picker.tsx | 4 +- frontend/i18n/config.ts | 9 +- frontend/i18n/messages.ts | 93 ++ .../messages/vi-VN/admin-announcements.json | 72 ++ .../i18n/messages/vi-VN/admin-billing.json | 343 +++++++ .../vi-VN/admin-content-moderation.json | 61 ++ .../messages/vi-VN/admin-conversation.json | 162 +++ frontend/i18n/messages/vi-VN/admin-files.json | 480 +++++++++ .../i18n/messages/vi-VN/admin-groups.json | 83 ++ frontend/i18n/messages/vi-VN/admin-login.json | 214 ++++ frontend/i18n/messages/vi-VN/admin-logs.json | 438 ++++++++ .../i18n/messages/vi-VN/admin-models.json | 512 ++++++++++ .../i18n/messages/vi-VN/admin-prompts.json | 75 ++ .../i18n/messages/vi-VN/admin-statistics.json | 76 ++ frontend/i18n/messages/vi-VN/admin-tools.json | 197 ++++ .../i18n/messages/vi-VN/admin-upstreams.json | 253 +++++ frontend/i18n/messages/vi-VN/admin-users.json | 296 ++++++ .../i18n/messages/vi-VN/announcements.json | 22 + frontend/i18n/messages/vi-VN/chat.json | 949 ++++++++++++++++++ frontend/i18n/messages/vi-VN/common.json | 155 +++ .../i18n/messages/vi-VN/conversation.json | 38 + frontend/i18n/messages/vi-VN/errors.json | 363 +++++++ frontend/i18n/messages/vi-VN/files.json | 221 ++++ frontend/i18n/messages/vi-VN/guide.json | 131 +++ .../i18n/messages/vi-VN/knowledge-bases.json | 126 +++ frontend/i18n/messages/vi-VN/login.json | 74 ++ frontend/i18n/messages/vi-VN/prompts.json | 63 ++ frontend/i18n/messages/vi-VN/recent.json | 181 ++++ frontend/i18n/messages/vi-VN/settings.json | 655 ++++++++++++ frontend/i18n/messages/vi-VN/share.json | 10 + frontend/i18n/resolve-error-message.ts | 154 +++ 48 files changed, 7457 insertions(+), 11 deletions(-) create mode 100644 backend/internal/application/skill/seed.go create mode 100644 backend/internal/application/skill/seed_test.go create mode 100644 backend/internal/application/skill/seeddata/chart-svg.md create mode 100644 backend/internal/application/skill/seeddata/code-review.md create mode 100644 backend/internal/application/skill/seeddata/diagram-svg.md create mode 100644 backend/internal/application/skill/seeddata/html-artifact.md create mode 100644 backend/internal/application/skill/seeddata/mermaid-diagram.md create mode 100644 backend/internal/application/skill/seeddata/polish-writing.md create mode 100644 backend/internal/application/skill/seeddata/report-writing.md create mode 100644 backend/internal/application/skill/seeddata/slide-outline.md create mode 100644 backend/internal/application/skill/seeddata/svg-icon.md create mode 100644 backend/internal/application/skill/seeddata/translate.md create mode 100644 frontend/i18n/messages/vi-VN/admin-announcements.json create mode 100644 frontend/i18n/messages/vi-VN/admin-billing.json create mode 100644 frontend/i18n/messages/vi-VN/admin-content-moderation.json create mode 100644 frontend/i18n/messages/vi-VN/admin-conversation.json create mode 100644 frontend/i18n/messages/vi-VN/admin-files.json create mode 100644 frontend/i18n/messages/vi-VN/admin-groups.json create mode 100644 frontend/i18n/messages/vi-VN/admin-login.json create mode 100644 frontend/i18n/messages/vi-VN/admin-logs.json create mode 100644 frontend/i18n/messages/vi-VN/admin-models.json create mode 100644 frontend/i18n/messages/vi-VN/admin-prompts.json create mode 100644 frontend/i18n/messages/vi-VN/admin-statistics.json create mode 100644 frontend/i18n/messages/vi-VN/admin-tools.json create mode 100644 frontend/i18n/messages/vi-VN/admin-upstreams.json create mode 100644 frontend/i18n/messages/vi-VN/admin-users.json create mode 100644 frontend/i18n/messages/vi-VN/announcements.json create mode 100644 frontend/i18n/messages/vi-VN/chat.json create mode 100644 frontend/i18n/messages/vi-VN/common.json create mode 100644 frontend/i18n/messages/vi-VN/conversation.json create mode 100644 frontend/i18n/messages/vi-VN/errors.json create mode 100644 frontend/i18n/messages/vi-VN/files.json create mode 100644 frontend/i18n/messages/vi-VN/guide.json create mode 100644 frontend/i18n/messages/vi-VN/knowledge-bases.json create mode 100644 frontend/i18n/messages/vi-VN/login.json create mode 100644 frontend/i18n/messages/vi-VN/prompts.json create mode 100644 frontend/i18n/messages/vi-VN/recent.json create mode 100644 frontend/i18n/messages/vi-VN/settings.json create mode 100644 frontend/i18n/messages/vi-VN/share.json diff --git a/backend/internal/app/app.go b/backend/internal/app/app.go index 832f012fe..4876bc891 100644 --- a/backend/internal/app/app.go +++ b/backend/internal/app/app.go @@ -359,6 +359,9 @@ func NewApp() (*App, error) { skillRepo := skillrepo.NewRepo(db) skillService := appskill.NewService(skillRepo) skillService.SetAuditWriter(auditService) + if err = skillService.EnsureBuiltinSeeds(context.Background()); err != nil { + return nil, fmt.Errorf("seed builtin skills: %w", err) + } conversationService.SetSkillResolver(skillService) skillHandler := skillhttp.NewHandler(skillService) skillModule := skillhttp.NewModule(skillHandler) diff --git a/backend/internal/application/auth/service.go b/backend/internal/application/auth/service.go index 4aa9ff898..3e606b376 100644 --- a/backend/internal/application/auth/service.go +++ b/backend/internal/application/auth/service.go @@ -812,7 +812,7 @@ func normalizeLocale(raw string) (string, error) { } normalized := strings.ReplaceAll(trimmed, "_", "-") switch normalized { - case "en", "en-US", "zh", "zh-CN": + case "en", "en-US", "zh", "zh-CN", "vi", "vi-VN": return normalized, nil default: return "", ErrInvalidLocale diff --git a/backend/internal/application/skill/seed.go b/backend/internal/application/skill/seed.go new file mode 100644 index 000000000..d856e3b1d --- /dev/null +++ b/backend/internal/application/skill/seed.go @@ -0,0 +1,217 @@ +package skill + +import ( + "context" + "embed" + "errors" + "fmt" + "io/fs" + "sort" + "strings" + + domainskill "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/skill" + "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/repository" + "gopkg.in/yaml.v3" +) + +//go:embed seeddata/*.md +var builtinSeedFS embed.FS + +const builtinSeedDir = "seeddata" + +// builtinSkillSeed 描述一份内置技能种子(SKILL.md + 元数据)。 +type builtinSkillSeed struct { + Trigger string + Title string + Description string + Markdown string + SortOrder int +} + +// builtinSkillFrontmatter 对应种子文件头部的 YAML 元数据。 +type builtinSkillFrontmatter struct { + Name string `yaml:"name"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Sort int `yaml:"sort"` +} + +// EnsureBuiltinSeeds 幂等补齐内置技能种子:缺失则创建;未被管理员改动过的 +// 种子同步为最新内容;管理员创建或编辑过的技能一律跳过。 +func (s *Service) EnsureBuiltinSeeds(ctx context.Context) error { + seeds, err := loadBuiltinSkillSeeds() + if err != nil { + return err + } + existing, err := s.listBuiltinSkillsForSeed(ctx) + if err != nil { + return err + } + byTrigger := make(map[string]domainskill.Skill, len(existing)) + for _, item := range existing { + byTrigger[normalizeTrigger(item.Trigger)] = item + } + for _, seed := range seeds { + current, exists := byTrigger[seed.Trigger] + if !exists { + if err := s.createBuiltinSeed(ctx, seed); err != nil { + return err + } + continue + } + if builtinSkillTouched(current) { + continue + } + if current.Title == seed.Title && + current.Description == seed.Description && + current.Markdown == seed.Markdown && + current.SortOrder == seed.SortOrder { + continue + } + if err := s.syncBuiltinSeed(ctx, current.ID, seed); err != nil { + return err + } + } + return nil +} + +// builtinSkillTouched 判断内置技能是否由管理员创建或编辑过: +// 种子写入时 created/updated 均为 0,管理员一旦改动 updated 会变为真实用户 ID。 +func builtinSkillTouched(item domainskill.Skill) bool { + return item.CreatedByUserID != 0 || item.UpdatedByUserID != 0 +} + +func (s *Service) createBuiltinSeed(ctx context.Context, seed builtinSkillSeed) error { + item, err := normalizeWriteInput(WriteInput{ + Title: seed.Title, + Trigger: seed.Trigger, + Description: seed.Description, + Markdown: seed.Markdown, + Enabled: true, + SortOrder: seed.SortOrder, + }, domainskill.ScopeBuiltin, 0, 0) + if err != nil { + return fmt.Errorf("seed skill %q: %w", seed.Trigger, err) + } + if _, err := s.repo.CreateSkill(ctx, item); err != nil && !errors.Is(err, repository.ErrDuplicate) { + return fmt.Errorf("create seed skill %q: %w", seed.Trigger, err) + } + return nil +} + +func (s *Service) syncBuiltinSeed(ctx context.Context, id uint, seed builtinSkillSeed) error { + title := seed.Title + description := seed.Description + markdown := seed.Markdown + sortOrder := seed.SortOrder + // 不携带 UpdatedByUserID,同步后仍视为“未改动”,后续升级可继续覆盖。 + _, err := s.repo.PatchSkill(ctx, id, repository.SkillPatch{ + Title: &title, + Description: &description, + Markdown: &markdown, + SortOrder: &sortOrder, + }) + if err != nil && !errors.Is(err, repository.ErrNotFound) { + return fmt.Errorf("sync seed skill %q: %w", seed.Trigger, err) + } + return nil +} + +func (s *Service) listBuiltinSkillsForSeed(ctx context.Context) ([]domainskill.Skill, error) { + const pageSize = 100 + var results []domainskill.Skill + for page := 1; ; page++ { + items, total, err := s.repo.ListSkills(ctx, repository.SkillListFilter{ + Scope: domainskill.ScopeBuiltin, + }, (page-1)*pageSize, pageSize) + if err != nil { + return nil, err + } + results = append(results, items...) + if int64(len(results)) >= total || len(items) == 0 { + return results, nil + } + } +} + +// loadBuiltinSkillSeeds 解析并校验全部内置技能种子文件。 +func loadBuiltinSkillSeeds() ([]builtinSkillSeed, error) { + entries, err := fs.ReadDir(builtinSeedFS, builtinSeedDir) + if err != nil { + return nil, err + } + seeds := make([]builtinSkillSeed, 0, len(entries)) + seen := make(map[string]struct{}, len(entries)) + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") { + continue + } + raw, err := builtinSeedFS.ReadFile(builtinSeedDir + "/" + entry.Name()) + if err != nil { + return nil, err + } + seed, err := parseBuiltinSkillSeed(entry.Name(), raw) + if err != nil { + return nil, fmt.Errorf("%s/%s: %w", builtinSeedDir, entry.Name(), err) + } + if _, exists := seen[seed.Trigger]; exists { + return nil, fmt.Errorf("%s/%s: duplicate seed trigger %q", builtinSeedDir, entry.Name(), seed.Trigger) + } + seen[seed.Trigger] = struct{}{} + seeds = append(seeds, seed) + } + if len(seeds) == 0 { + return nil, errors.New("no builtin skill seeds found") + } + sort.Slice(seeds, func(i, j int) bool { + if seeds[i].SortOrder != seeds[j].SortOrder { + return seeds[i].SortOrder < seeds[j].SortOrder + } + return seeds[i].Trigger < seeds[j].Trigger + }) + return seeds, nil +} + +func parseBuiltinSkillSeed(name string, raw []byte) (builtinSkillSeed, error) { + content := strings.TrimSpace(strings.ReplaceAll(string(raw), "\r\n", "\n")) + const open = "---\n" + if !strings.HasPrefix(content, open) { + return builtinSkillSeed{}, errors.New("missing frontmatter") + } + rest := strings.TrimPrefix(content, open) + end := strings.Index(rest, "\n---") + if end < 0 { + return builtinSkillSeed{}, errors.New("unterminated frontmatter") + } + var frontmatter builtinSkillFrontmatter + if err := yaml.Unmarshal([]byte(rest[:end]), &frontmatter); err != nil { + return builtinSkillSeed{}, fmt.Errorf("invalid frontmatter: %w", err) + } + body := strings.TrimSpace(rest[end+len("\n---"):]) + + trigger := normalizeTrigger(frontmatter.Name) + title := strings.TrimSpace(frontmatter.Title) + description := strings.TrimSpace(frontmatter.Description) + if trigger == "" || runeCount(trigger) > maxSkillTriggerLength { + return builtinSkillSeed{}, fmt.Errorf("invalid trigger %q", frontmatter.Name) + } + if title == "" || runeCount(title) > maxSkillTitleLength { + return builtinSkillSeed{}, fmt.Errorf("invalid title %q", title) + } + if runeCount(description) > maxSkillDescriptionLength { + return builtinSkillSeed{}, errors.New("description exceeds limit") + } + if body == "" || runeCount(body) > maxSkillMarkdownLength { + return builtinSkillSeed{}, errors.New("markdown empty or exceeds limit") + } + if frontmatter.Sort <= 0 { + return builtinSkillSeed{}, errors.New("sort must be positive") + } + return builtinSkillSeed{ + Trigger: trigger, + Title: title, + Description: description, + Markdown: body, + SortOrder: frontmatter.Sort, + }, nil +} diff --git a/backend/internal/application/skill/seed_test.go b/backend/internal/application/skill/seed_test.go new file mode 100644 index 000000000..fa8cf3ceb --- /dev/null +++ b/backend/internal/application/skill/seed_test.go @@ -0,0 +1,227 @@ +package skill + +import ( + "context" + "testing" + + domainskill "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/domain/skill" + "github.com/DEEIX-AI/DEEIX-Chat/backend/internal/repository" +) + +type seedFakeRepo struct { + items []domainskill.Skill + nextID uint + createCalls int + patchCalls int +} + +func (r *seedFakeRepo) ListSkills(_ context.Context, filter repository.SkillListFilter, offset int, limit int) ([]domainskill.Skill, int64, error) { + if limit <= 0 { + limit = 20 + } + matched := make([]domainskill.Skill, 0) + for _, item := range r.items { + if filter.Scope != "" && item.Scope != filter.Scope { + continue + } + if filter.OwnerUserID != nil && item.OwnerUserID != *filter.OwnerUserID { + continue + } + matched = append(matched, item) + } + total := int64(len(matched)) + if offset >= len(matched) { + return nil, total, nil + } + end := offset + limit + if end > len(matched) { + end = len(matched) + } + return matched[offset:end], total, nil +} + +func (r *seedFakeRepo) GetSkill(_ context.Context, id uint) (*domainskill.Skill, error) { + for _, item := range r.items { + if item.ID == id { + result := item + return &result, nil + } + } + return nil, repository.ErrNotFound +} + +func (r *seedFakeRepo) CreateSkill(_ context.Context, item *domainskill.Skill) (*domainskill.Skill, error) { + for _, existing := range r.items { + if existing.Scope == item.Scope && existing.OwnerUserID == item.OwnerUserID && existing.Trigger == item.Trigger { + return nil, repository.ErrDuplicate + } + } + if r.nextID == 0 { + r.nextID = 1 + } + item.ID = r.nextID + r.nextID++ + r.items = append(r.items, *item) + r.createCalls++ + result := item + return result, nil +} + +func (r *seedFakeRepo) PatchSkill(_ context.Context, id uint, patch repository.SkillPatch) (*domainskill.Skill, error) { + for index := range r.items { + if r.items[index].ID != id { + continue + } + if patch.Title != nil { + r.items[index].Title = *patch.Title + } + if patch.Trigger != nil { + r.items[index].Trigger = *patch.Trigger + } + if patch.Description != nil { + r.items[index].Description = *patch.Description + } + if patch.Markdown != nil { + r.items[index].Markdown = *patch.Markdown + } + if patch.Enabled != nil { + r.items[index].Enabled = *patch.Enabled + } + if patch.SortOrder != nil { + r.items[index].SortOrder = *patch.SortOrder + } + if patch.UpdatedByUserIDSet { + r.items[index].UpdatedByUserID = patch.UpdatedByUserID + } + r.patchCalls++ + result := r.items[index] + return &result, nil + } + return nil, repository.ErrNotFound +} + +func (r *seedFakeRepo) DeleteSkill(_ context.Context, id uint) error { + for index, item := range r.items { + if item.ID == id { + r.items = append(r.items[:index], r.items[index+1:]...) + return nil + } + } + return repository.ErrNotFound +} + +func TestBuiltinSkillSeedsLoadAndValidate(t *testing.T) { + seeds, err := loadBuiltinSkillSeeds() + if err != nil { + t.Fatalf("expected seeds to load, got %v", err) + } + if len(seeds) < 8 { + t.Fatalf("expected at least 8 builtin seeds, got %d", len(seeds)) + } + triggers := map[string]struct{}{} + for _, seed := range seeds { + if seed.Trigger == "" || seed.Title == "" || seed.Markdown == "" { + t.Fatalf("seed %q has empty required fields", seed.Trigger) + } + if seed.SortOrder <= 0 { + t.Fatalf("seed %q must declare positive sort", seed.Trigger) + } + if _, exists := triggers[seed.Trigger]; exists { + t.Fatalf("duplicate seed trigger %q", seed.Trigger) + } + triggers[seed.Trigger] = struct{}{} + } + if _, exists := triggers["diagram-svg"]; !exists { + t.Fatal("expected diagram-svg seed to exist") + } +} + +func TestEnsureBuiltinSeedsCreatesAndIsIdempotent(t *testing.T) { + repo := &seedFakeRepo{} + service := NewService(repo) + if err := service.EnsureBuiltinSeeds(context.Background()); err != nil { + t.Fatalf("expected seed run to succeed, got %v", err) + } + seeds, err := loadBuiltinSkillSeeds() + if err != nil { + t.Fatalf("expected seeds to load, got %v", err) + } + if len(repo.items) != len(seeds) { + t.Fatalf("expected %d seeded skills, got %d", len(seeds), len(repo.items)) + } + for _, item := range repo.items { + if item.Scope != domainskill.ScopeBuiltin || item.OwnerUserID != 0 { + t.Fatalf("seed %q must be builtin scope with owner 0", item.Trigger) + } + if !item.Enabled { + t.Fatalf("seed %q must be enabled", item.Trigger) + } + if item.CreatedByUserID != 0 || item.UpdatedByUserID != 0 { + t.Fatalf("seed %q must look untouched", item.Trigger) + } + } + if err := service.EnsureBuiltinSeeds(context.Background()); err != nil { + t.Fatalf("expected second seed run to succeed, got %v", err) + } + if repo.createCalls != len(seeds) || repo.patchCalls != 0 { + t.Fatalf("expected no writes on second run, got create=%d patch=%d", repo.createCalls, repo.patchCalls) + } +} + +func TestEnsureBuiltinSeedsSyncsUntouchedAndSkipsEdited(t *testing.T) { + repo := &seedFakeRepo{} + service := NewService(repo) + if err := service.EnsureBuiltinSeeds(context.Background()); err != nil { + t.Fatalf("expected seed run to succeed, got %v", err) + } + var stale domainskill.Skill + var edited domainskill.Skill + for _, item := range repo.items { + if item.Trigger == "diagram-svg" { + stale = item + } + if item.Trigger == "code-review" { + edited = item + } + } + if stale.ID == 0 || edited.ID == 0 { + t.Fatal("expected target seeds to exist") + } + // 模拟管理员改过内容的技能。 + if _, err := repo.PatchSkill(context.Background(), edited.ID, repository.SkillPatch{ + Markdown: strPtr("admin rewrite"), + UpdatedByUserIDSet: true, + UpdatedByUserID: 9, + }); err != nil { + t.Fatalf("patch failed: %v", err) + } + // 模拟旧版本种子内容。 + if _, err := repo.PatchSkill(context.Background(), stale.ID, repository.SkillPatch{ + Markdown: strPtr("old seed content"), + }); err != nil { + t.Fatalf("patch failed: %v", err) + } + + if err := service.EnsureBuiltinSeeds(context.Background()); err != nil { + t.Fatalf("expected reseed to succeed, got %v", err) + } + var syncedStale domainskill.Skill + for _, item := range repo.items { + if item.Trigger == "diagram-svg" { + syncedStale = item + } + if item.Trigger == "code-review" && item.Markdown != "admin rewrite" { + t.Fatal("admin-edited skill must not be overwritten") + } + } + if syncedStale.Markdown == "old seed content" { + t.Fatal("untouched stale seed must be synced to latest content") + } + if syncedStale.UpdatedByUserID != 0 { + t.Fatalf("synced seed must remain untouched, got updated_by=%d", syncedStale.UpdatedByUserID) + } +} + +func strPtr(value string) *string { + return &value +} diff --git a/backend/internal/application/skill/seeddata/chart-svg.md b/backend/internal/application/skill/seeddata/chart-svg.md new file mode 100644 index 000000000..93a731c7a --- /dev/null +++ b/backend/internal/application/skill/seeddata/chart-svg.md @@ -0,0 +1,47 @@ +--- +name: chart-svg +title: SVG Data Visualization +description: Draw accurate, well-labeled SVG charts (bar, line, area, pie, scatter) computed from the user's data, with correct scales, axes, and legends. +sort: 50 +--- + +# SVG Data Visualization + +Render the user's data as precise hand-computed SVG charts in ` ```svg ` blocks. + +## When to use + +- "Chart/graph/plot this data", comparisons, trends over time, composition, distribution. +- Small-to-medium datasets (≤ ~30 points). For larger data, aggregate first and say so. + +## Output contract + +- One ` ```svg ` block per chart, self-contained, light background rect, `viewBox` around `0 0 760 460`, system font stack via root `font-family`. +- Always include: title (16–18px semibold), axis labels with units, tick values, and — when more than one series — a legend. Direct-label lines/bars when space allows (legend then optional). +- After the chart, include the underlying numbers as a small markdown table so the user can verify and copy them. + +## Chart choice + +- Time series / trend → line or area. Composition of a whole (≤ 5 slices) → bar preferred, pie only if the user insists. Comparison across categories → sorted horizontal bars. Relationship of two variables → scatter. Never 3D, never dual axes without an explicit warning. + +## Accuracy rules (non-negotiable) + +- Compute the scale honestly: nice tick steps (1/2/5 × 10^n), start bar/line charts at zero unless there's a stated reason, and if a truncated axis is truly needed, mark it clearly. +- Map values to pixels with an explicit formula (`x = left + (value - min) / (max - min) * width`) and compute each coordinate; round to 1 decimal. A chart that misencodes its data is a defect regardless of looks. +- Label exact values on bars (10–12px, above the bar) when there are ≤ 12 bars. +- Pie: convert percentages to arc endpoints with `path` arcs (not stroke-dash tricks), first slice at 12 o'clock clockwise, slices ≥ 3% get labels, everything smaller groups into "Other". + +## Design rules + +- Palette: `#2563eb`, `#16a34a`, `#d97706`, `#dc2626`, `#7c3aed`; grid lines `#e2e8f0`; text `#0f172a` / secondary `#475569`. +- Gridlines: horizontal only, behind data, no chartjunk (no gradients, shadows, decorative icons). +- 16px margins around the plot area; series labels never overlap — if they collide, rotate or shorten them deliberately. + +## Quality checklist + +- [ ] Every plotted pixel traces back to the data (recompute two points mentally). +- [ ] Axes show units; ticks are round numbers; zero baseline unless flagged. +- [ ] Title answers "what am I looking at"; legend/direct labels identify every series. +- [ ] Data table below matches the chart exactly. + +State any assumption (aggregation applied, excluded rows, currency) in one sentence after the table. Never invent data points to fill gaps — show gaps as gaps. diff --git a/backend/internal/application/skill/seeddata/code-review.md b/backend/internal/application/skill/seeddata/code-review.md new file mode 100644 index 000000000..0e486cf31 --- /dev/null +++ b/backend/internal/application/skill/seeddata/code-review.md @@ -0,0 +1,49 @@ +--- +name: code-review +title: Code Review +description: Systematic code review with severity-tagged findings (correctness, security, performance, readability), concrete fixes, and praised strengths. +sort: 90 +--- + +# Code Review + +Review provided code (snippet, file, diff, or PR) systematically and return prioritized, actionable findings. + +## When to use + +- "Review this code/PR/diff", pre-merge checks, security or performance-focused passes, or reviewing AI-generated code before committing. + +## Review pass order + +1. **Understand intent** — what is this code supposed to do? State your understanding in one sentence; findings are judged against intent. +2. **Correctness** — logic errors, edge cases (empty/null/zero/overflow), error paths, race conditions, off-by-one, resource leaks, broken contracts. +3. **Security** — injection (SQL/command/template), authz checks, secret handling, unsafe deserialization, SSRF, path traversal, missing input validation at trust boundaries. +4. **Performance & cost** — N+1 queries, unbounded loops/collections, repeated work in hot paths, missing pagination, sync calls that should be async. +5. **Maintainability** — naming, dead code, duplicated logic, functions > ~50 lines, missing error context, misleading comments, test coverage of changed behavior. + +## Output format + +Start with a one-paragraph verdict: overall assessment + whether it's safe to merge/ship. + +Then findings, each with: + +- **[Severity] Title** — severity: `P0 must-fix` (bug/security/data loss), `P1 should-fix` (likely bug, major design flaw), `P2 nice-to-fix` (style, minor perf), `P3 note` (consideration, question). +- Location: file/function/line reference as given (`auth.go:42`, `parseToken` in the diff hunk). +- Why it matters: 1–2 sentences with the concrete failure scenario ("an empty `items` list returns `ErrNotFound`, callers treat it as 404"). +- Fix: a concrete patch suggestion (short code block) or a precise instruction. + +Order findings by severity, then by impact. End with **Strengths** — 1–3 things done well (guard clauses, good naming, tight tests) — and, if meaningful, **Not reviewed** (aspects out of scope: infra, dependencies I couldn't see). + +## Rules + +- Verify before flagging: re-read the surrounding code/context for each suspected issue; false positives erode trust. If unsure, mark it a question ("P3: is X guaranteed non-nil here?") instead of asserting a bug. +- Flag what's actually wrong, not style differences from your preferences. Follow the project's existing conventions unless they cause bugs. +- Every P0/P1 must come with a fix suggestion or a clear mitigation path. +- For diffs, review ONLY changed lines and their blast radius — don't pad with pre-existing issues unless they interact with the change (then say they're pre-existing). +- Don't rewrite the whole thing; suggest the minimal sound change. + +## Quality checklist + +- [ ] Verdict first; findings sorted; each has location, scenario, and fix. +- [ ] No false alarms; uncertain items phrased as questions. +- [ ] Strengths and out-of-scope noted. diff --git a/backend/internal/application/skill/seeddata/diagram-svg.md b/backend/internal/application/skill/seeddata/diagram-svg.md new file mode 100644 index 000000000..52a0f5061 --- /dev/null +++ b/backend/internal/application/skill/seeddata/diagram-svg.md @@ -0,0 +1,54 @@ +--- +name: diagram-svg +title: SVG Diagram Builder +description: Design clear, self-contained SVG diagrams (flowcharts, architecture, sequence, ER, network) that render inline and download cleanly. Use when the user asks for a diagram, visual, or schematic. +sort: 10 +--- + +# SVG Diagram Builder + +Produce polished, standalone SVG diagrams the chat UI can preview and the user can download as `.svg`. + +## When to use + +- Flowcharts, architecture / system diagrams, sequence-style interactions, ER diagrams, network topologies, org charts, pipelines, timelines. +- Anything the user calls a "diagram", "schematic", "visual overview", "map of how X works". +- Prefer the `mermaid-diagram` skill when the user explicitly asks for Mermaid or wants an editable text source. + +## Output contract + +- Emit exactly one fenced block per diagram: ` ```svg ` … ` ``` `. Never inline SVG in raw HTML. +- The SVG must be fully self-contained: no external fonts, images, CSS, or scripts. +- Start with `` and use a root like: + +``` + +``` + +- Draw an explicit light background (``) so the diagram stays readable on dark UI themes. +- Keep width between 720–1200 viewBox units. Do not set fixed `width`/`height` attributes; let `viewBox` scale. + +## Design rules + +- **Palette (max 5 colors):** neutral container `#f8fafc` / stroke `#334155`; primary accent `#2563eb`; success `#16a34a`; warning `#d97706`; danger `#dc2626`. Text `#0f172a`, secondary text `#475569`. +- Consistent corner radius (8–12), one stroke weight for boxes (1.5–2) and one for connectors (1.5). Arrowheads via `` with `markerWidth/markerHeight` around 8. +- One font family, 2 sizes: title 18–20 semibold, labels 13–14. Node labels must be short (≤ 4 words); put detail in a legend or caption below the diagram. +- Every node: rounded rect + centered text. Compute text width honestly (≈7px per character at 14px) and pad boxes 16px horizontal, 10px vertical. Never let text overflow a box. +- Connectors are orthogonal or smooth curves with elbow routing; leave ≥ 24px gap between any line and unrelated boxes. Label conditional edges (`yes` / `no`, `on success`) in 12–13px on a small white-backed pill. +- Group related nodes with a light container (`fill="#f1f5f9"`, dashed stroke) and a 12–13px uppercase group label. + +## Layout workflow + +1. List the nodes and edges first (mentally or in a short plan). Choose a direction: top-to-bottom for flows, left-to-right for pipelines and layered architecture. +2. Assign rows/columns; identical node types share size. Compute coordinates on a grid (multiples of 8) before writing any tag. +3. Draw containers → nodes → connectors → labels → legend. Title top-left, optional caption bottom. +4. Re-verify every coordinate pair against your grid math; overlapping or crossing elements are defects. + +## Quality checklist (self-check before answering) + +- [ ] Renders standalone (paste into any browser) with correct namespaces. +- [ ] No text overflows, overlaps, or clipped edges; all arrows touch their target. +- [ ] Colors/typography follow the palette; total distinct colors ≤ 5. +- [ ] Meaning is graspable in < 10 seconds; legend present if shapes encode anything. + +After the block, add one or two sentences explaining what the diagram shows and how to read it. If requirements were ambiguous, state the assumption you drew with (e.g. "assumed synchronous calls") instead of asking first. diff --git a/backend/internal/application/skill/seeddata/html-artifact.md b/backend/internal/application/skill/seeddata/html-artifact.md new file mode 100644 index 000000000..64ff6d8aa --- /dev/null +++ b/backend/internal/application/skill/seeddata/html-artifact.md @@ -0,0 +1,45 @@ +--- +name: html-artifact +title: Interactive HTML Artifact +description: Build single-file, self-contained interactive HTML pages (calculators, dashboards, simulators, forms) that preview safely in the chat and download as one .html file. +sort: 30 +--- + +# Interactive HTML Artifact + +Produce single-file interactive HTML the chat preview can render in a sandboxed frame and the user can download as one `.html`. + +## When to use + +- Calculators, converters, planners, what-if simulators, interactive tables/dashboards, visual demos, simple games, styled one-pagers (invoices, resumes, posters). +- When interactivity (inputs, live recompute, sorting/filtering) adds real value over static markdown. + +## Output contract + +- Exactly one fenced ` ```html ` block containing a complete document: ``, ``, ``, ``. +- **Zero external dependencies**: no CDN scripts, no web fonts, no fetch/XHR, no images beyond data URIs or inline SVG. The file must work offline. +- All CSS in one `