From 912d55d864efee44bf6f17c18c4dff77dfd0a86a Mon Sep 17 00:00:00 2001 From: JoachimLK Date: Tue, 28 Apr 2026 15:12:26 +0200 Subject: [PATCH 1/3] feat: add AI chatbot feature with configuration, access control, and attachment management - Implemented loadAiConfig to resolve AI configurations based on organization and purpose. - Created requireChatbotAccess to enforce authentication and feature flag checks for chatbot endpoints. - Developed chatbotAttachments for managing file uploads and in-memory storage of attachments. - Added chatbotSources to extract structured sources from chatbot tool results. - Introduced featureFlags utility to manage feature flag resolution on the server. - Defined shared types for chatbot functionality, including messages, attachments, and sources. - Established a feature flag registry for controlling the availability of the new AI chatbot experience. --- .env.example | 12 + SELF-HOSTING.md | 46 +- app/components/AiConfigForm.vue | 609 ++ app/components/AppTopBar.vue | 33 +- app/components/ChatbotAgentManagerModal.vue | 291 + app/components/ChatbotAgentPicker.vue | 105 + app/components/ChatbotModelPicker.vue | 128 + app/components/ChatbotSidebar.vue | 339 ++ app/components/ChatbotSourcesPanel.vue | 125 + app/components/ConversationItem.vue | 146 + app/components/JobSubNavActions.vue | 321 ++ app/components/MarkdownDescription.vue | 2 +- app/components/OrgSwitcher.vue | 2 +- app/components/ScoreBreakdown.vue | 82 +- app/composables/useChatbot.ts | 620 +++ app/composables/useFeatureFlag.ts | 85 + app/layouts/dashboard.vue | 5 +- app/pages/dashboard/chatbot/[[id]].vue | 624 +++ app/pages/dashboard/jobs/[id]/ai-analysis.vue | 2 + .../dashboard/jobs/[id]/application-form.vue | 2 + app/pages/dashboard/jobs/[id]/candidates.vue | 2 + app/pages/dashboard/jobs/[id]/index.vue | 311 +- app/pages/dashboard/jobs/[id]/settings.vue | 2 + app/pages/dashboard/jobs/new.vue | 8 +- app/pages/dashboard/settings/ai.vue | 462 -- app/pages/dashboard/settings/ai/[id].vue | 121 + app/pages/dashboard/settings/ai/index.vue | 326 ++ app/pages/dashboard/settings/ai/new.vue | 98 + app/types/router.d.ts | 7 + nuxt.config.ts | 7 + public/reqcore-emoji-128-transparent.png | Bin 0 -> 19257 bytes public/reqcore-emoji-128-transparent.webp | Bin 0 -> 2146 bytes public/reqcore-emoji-128.png | Bin 0 -> 18949 bytes public/reqcore-emoji-256-transparent.png | Bin 0 -> 75921 bytes public/reqcore-emoji-256-transparent.webp | Bin 0 -> 4722 bytes public/reqcore-emoji-256.png | Bin 0 -> 75542 bytes server/api/ai-analysis/stats.get.ts | 44 +- server/api/ai-config/[id].delete.ts | 56 + server/api/ai-config/[id].get.ts | 42 + server/api/ai-config/[id].patch.ts | 71 + server/api/ai-config/[id]/set-default.post.ts | 58 + .../{ => [id]}/test-connection.post.ts | 46 +- .../api/ai-config/generate-criteria.post.ts | 28 +- server/api/ai-config/index.get.ts | 23 +- server/api/ai-config/index.post.ts | 107 +- server/api/applications/[id]/analyze.post.ts | 23 +- server/api/chatbot/agents/[id].delete.ts | 31 + server/api/chatbot/agents/[id].patch.ts | 90 + server/api/chatbot/agents/index.get.ts | 37 + server/api/chatbot/agents/index.post.ts | 86 + server/api/chatbot/chat.post.ts | 442 ++ .../api/chatbot/conversations/[id].delete.ts | 28 + server/api/chatbot/conversations/[id].get.ts | 64 + .../api/chatbot/conversations/[id].patch.ts | 111 + server/api/chatbot/conversations/index.get.ts | 43 + .../api/chatbot/conversations/index.post.ts | 106 + server/api/chatbot/folders/[id].delete.ts | 31 + server/api/chatbot/folders/[id].patch.ts | 52 + server/api/chatbot/folders/index.get.ts | 31 + server/api/chatbot/folders/index.post.ts | 69 + server/api/chatbot/upload.post.ts | 96 + .../migrations/0025_chatbot_persistence.sql | 94 + .../migrations/0026_multi_ai_configs.sql | 43 + .../migrations/meta/0025_snapshot.json | 4926 +++++++++++++++++ server/database/migrations/meta/_journal.json | 14 + server/database/schema/app.ts | 141 +- server/utils/ai/autoScore.ts | 13 +- server/utils/ai/chatTools.ts | 479 ++ server/utils/ai/loadConfig.ts | 47 + server/utils/ai/provider.ts | 64 +- server/utils/chatbotAccess.ts | 31 + server/utils/chatbotAttachments.ts | 86 + server/utils/chatbotSources.ts | 165 + server/utils/featureFlags.ts | 97 + server/utils/posthog.ts | 10 + server/utils/schemas/scoring.ts | 16 +- shared/chatbot.ts | 183 + shared/feature-flags.ts | 116 + 78 files changed, 12325 insertions(+), 938 deletions(-) create mode 100644 app/components/AiConfigForm.vue create mode 100644 app/components/ChatbotAgentManagerModal.vue create mode 100644 app/components/ChatbotAgentPicker.vue create mode 100644 app/components/ChatbotModelPicker.vue create mode 100644 app/components/ChatbotSidebar.vue create mode 100644 app/components/ChatbotSourcesPanel.vue create mode 100644 app/components/ConversationItem.vue create mode 100644 app/components/JobSubNavActions.vue create mode 100644 app/composables/useChatbot.ts create mode 100644 app/composables/useFeatureFlag.ts create mode 100644 app/pages/dashboard/chatbot/[[id]].vue delete mode 100644 app/pages/dashboard/settings/ai.vue create mode 100644 app/pages/dashboard/settings/ai/[id].vue create mode 100644 app/pages/dashboard/settings/ai/index.vue create mode 100644 app/pages/dashboard/settings/ai/new.vue create mode 100644 app/types/router.d.ts create mode 100644 public/reqcore-emoji-128-transparent.png create mode 100644 public/reqcore-emoji-128-transparent.webp create mode 100644 public/reqcore-emoji-128.png create mode 100644 public/reqcore-emoji-256-transparent.png create mode 100644 public/reqcore-emoji-256-transparent.webp create mode 100644 public/reqcore-emoji-256.png create mode 100644 server/api/ai-config/[id].delete.ts create mode 100644 server/api/ai-config/[id].get.ts create mode 100644 server/api/ai-config/[id].patch.ts create mode 100644 server/api/ai-config/[id]/set-default.post.ts rename server/api/ai-config/{ => [id]}/test-connection.post.ts (50%) create mode 100644 server/api/chatbot/agents/[id].delete.ts create mode 100644 server/api/chatbot/agents/[id].patch.ts create mode 100644 server/api/chatbot/agents/index.get.ts create mode 100644 server/api/chatbot/agents/index.post.ts create mode 100644 server/api/chatbot/chat.post.ts create mode 100644 server/api/chatbot/conversations/[id].delete.ts create mode 100644 server/api/chatbot/conversations/[id].get.ts create mode 100644 server/api/chatbot/conversations/[id].patch.ts create mode 100644 server/api/chatbot/conversations/index.get.ts create mode 100644 server/api/chatbot/conversations/index.post.ts create mode 100644 server/api/chatbot/folders/[id].delete.ts create mode 100644 server/api/chatbot/folders/[id].patch.ts create mode 100644 server/api/chatbot/folders/index.get.ts create mode 100644 server/api/chatbot/folders/index.post.ts create mode 100644 server/api/chatbot/upload.post.ts create mode 100644 server/database/migrations/0025_chatbot_persistence.sql create mode 100644 server/database/migrations/0026_multi_ai_configs.sql create mode 100644 server/database/migrations/meta/0025_snapshot.json create mode 100644 server/utils/ai/chatTools.ts create mode 100644 server/utils/ai/loadConfig.ts create mode 100644 server/utils/chatbotAccess.ts create mode 100644 server/utils/chatbotAttachments.ts create mode 100644 server/utils/chatbotSources.ts create mode 100644 server/utils/featureFlags.ts create mode 100644 shared/chatbot.ts create mode 100644 shared/feature-flags.ts diff --git a/.env.example b/.env.example index a110ad94..74d400de 100644 --- a/.env.example +++ b/.env.example @@ -74,6 +74,18 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000 # POSTHOG_PUBLIC_KEY=phc_... # EU data center (default). Use https://us.i.posthog.com for US. # POSTHOG_HOST=https://eu.i.posthog.com +# Personal API key with "Feature Flags: read" scope. When set, the server +# evaluates feature flags locally (no per-request HTTP round trip). +# POSTHOG_FEATURE_FLAGS_KEY=phx_... + +# ─── Optional: Feature Flag Overrides (no PostHog required) ───────────────── +# Force any flag on or off without running PostHog. The full list of available +# flags lives in shared/feature-flags.ts. Variable name pattern: +# FEATURE_FLAG_ +# Accepted values: true / false / 1 / 0 / on / off (or a variant key for +# multivariate flags). Env overrides win over PostHog rollouts. +# Example — enable the new chatbot experience for everyone on this instance: +# FEATURE_FLAG_CHATBOT_EXPERIENCE=true # ─── Optional: OIDC SSO (Keycloak, Authentik, Authelia, Okta, etc.) ────────── # Enable Single Sign-On via any OIDC-compliant identity provider. diff --git a/SELF-HOSTING.md b/SELF-HOSTING.md index cc487ccd..2399bab1 100644 --- a/SELF-HOSTING.md +++ b/SELF-HOSTING.md @@ -17,9 +17,10 @@ Everything you need to deploy, manage, and update your own Reqcore applicant tra 9. [Custom Domain & HTTPS](#custom-domain--https) 10. [Email Configuration](#email-configuration) 11. [Security Best Practices](#security-best-practices) -12. [Monitoring & Health Checks](#monitoring--health-checks) -13. [Troubleshooting](#troubleshooting) -14. [FAQ](#faq) +12. [Feature Flags](#feature-flags) +13. [Monitoring & Health Checks](#monitoring--health-checks) +14. [Troubleshooting](#troubleshooting) +15. [FAQ](#faq) --- @@ -592,6 +593,45 @@ The SSO button appears automatically on the sign-in and sign-up pages. --- +## Feature Flags + +Reqcore ships some features behind **feature flags** so they can be tested in production before being released to everyone. The full list of flags lives in [`shared/feature-flags.ts`](shared/feature-flags.ts). + +### How it works for self-hosters + +Every flag has a safe **default value** baked into the code. You get that default automatically — **no PostHog account or external service required**. + +If you want to opt into an experimental feature (or disable a stable one), set an environment variable matching the pattern: + +```bash +FEATURE_FLAG_=true +``` + +Examples: + +```bash +# Enable the new chatbot experience for everyone on this instance +FEATURE_FLAG_CHATBOT_EXPERIENCE=true + +# Force-disable a flag that defaults to on +FEATURE_FLAG_SOMETHING_ELSE=false +``` + +Restart the container after editing `.env`. Env-var overrides win over any PostHog rollout, so this is the authoritative knob for self-hosters. + +### Resolution order + +1. URL query string (e.g. `?ff_chatbot-experience=true`) — handy for QA +2. Env var override (`FEATURE_FLAG_*`) — what you'll use 99% of the time +3. PostHog rollout — only applies when `POSTHOG_PUBLIC_KEY` is set +4. Registry default from `shared/feature-flags.ts` + +### I want to use PostHog for gradual rollout + +Optional. Set `POSTHOG_PUBLIC_KEY` and `POSTHOG_HOST` in `.env`, then create a flag in your PostHog project with a key matching the registry (e.g. `chatbot-experience`). For server-side flags without per-request HTTP calls, also set `POSTHOG_FEATURE_FLAGS_KEY` to a personal API key with the **Feature Flags: read** scope. + +--- + ## Monitoring & Health Checks ### Built-in System Health Dashboard diff --git a/app/components/AiConfigForm.vue b/app/components/AiConfigForm.vue new file mode 100644 index 00000000..54d826f9 --- /dev/null +++ b/app/components/AiConfigForm.vue @@ -0,0 +1,609 @@ + + + diff --git a/app/components/AppTopBar.vue b/app/components/AppTopBar.vue index e87f0f03..0bdbb6e0 100644 --- a/app/components/AppTopBar.vue +++ b/app/components/AppTopBar.vue @@ -6,6 +6,7 @@ import { ChevronDown, Menu, X, Users, ChevronLeft, LayoutDashboard, Calendar, ArrowUpCircle, Cloud, Server, Sparkles, Radio, History, + MessageCircle, } from 'lucide-vue-next' const route = useRoute() @@ -103,6 +104,8 @@ const { data: feedbackConfig } = useFetch('/api/feedback/config', { const isFeedbackEnabled = computed(() => feedbackConfig.value?.enabled === true) +const showChatbot = useFeatureFlagEnabled('chatbot-experience') + const jobTabs = computed(() => { if (!activeJobId.value) return [] const base = `/dashboard/jobs/${activeJobId.value}` @@ -131,6 +134,28 @@ const mainNav: Array<{ label: string; to: string; icon: typeof Briefcase; exact: { label: 'Settings', to: '/dashboard/settings', icon: Settings, exact: false }, ] +// Items shown only when their feature flag is enabled. Filtered into mainNav +// reactively so the gating happens at render time (PostHog flags load async). +const flaggedNav = computed(() => { + const items: Array<{ label: string; to: string; icon: typeof Briefcase; exact: boolean; afterLabel: string }> = [] + if (showChatbot.value) { + items.push({ label: 'Assistant', to: '/dashboard/chatbot', icon: MessageCircle, exact: false, afterLabel: 'AI Analysis' }) + } + return items +}) + +const navItems = computed(() => { + const merged = [...mainNav] + for (const item of flaggedNav.value) { + const idx = merged.findIndex((n) => n.label === item.afterLabel) + const insertAt = idx >= 0 ? idx + 1 : merged.length + merged.splice(insertAt, 0, { + label: item.label, to: item.to, icon: item.icon, exact: item.exact, + }) + } + return merged +}) + function isActiveRoute(to: string, exact: boolean) { const localizedTo = localePath(to) if (exact) return route.path === localizedTo @@ -196,7 +221,7 @@ onUnmounted(() => {
-
+
@@ -492,7 +517,7 @@ onUnmounted(() => { >