Root cause: when typing fast + Enter, previous send hadn't finished
clearing the editor, so new text accumulated. Fix: keydown handler
captures content snapshot and clears Quill SYNCHRONOUSLY, then sends
via sendContent(). No setTimeout, no race condition.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of waiting for API response before showing the message,
the editor clears and message appears in DOM immediately. The API
call happens in the background. Real message ID replaces temp ID
in state for polling dedup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: appendMessage() had no dedup check, so both send() and polling
could add the same message. Now appendMessage() checks if msg.id already
exists in state.messages[convId] before adding — guaranteed single display.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The actual bug: Composer.send() appends message to DOM via appendMessage(),
then 1-5s later the polling loop fetches the same message from API and
appends it again. Fix: track sent messages in state.messages[convId] so
the polling dedup check (msg.id > newestId) filters them out.
Also removed debug logging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Added _isSending flag to Composer.send() — blocks concurrent sends
- Added Quill keyboard binding override for Enter (handler returns false)
- Added stopImmediatePropagation() to DOM keydown handler
- Added _isCreating flag to NewConversation.send()
- Flags reset in finally blocks to handle errors gracefully
Fixes: messages appearing twice when user presses Enter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- "Dane kontaktowe" chip now says "TYLKO do firm które polecileś powyżej"
- Added prompt rule: follow-up contact requests = only previously recommended firms
- Each contact entry must include short reason WHY the firm was recommended
- Prevents AI from dumping all 15 matcher results when user asks for contacts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. When company_matcher returns [], keep full company list as fallback instead of leaving AI with zero data
2. Add explicit "no results" instruction in prompt to prevent hallucinated company names
3. Hide cost badge chip from non-admin users (IS_ADMIN gate)
4. Add 60s AbortController timeout on streaming fetch to prevent hung connections
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Badge now uses pill-shaped colored chips instead of tiny gray text:
- Blue chip: model name (Flash/Lite/Pro)
- Yellow chip: thinking level (szybki/analiza/głęboka analiza)
- Green chip: response time
- Green chip: cost
Much more readable than the previous 11px gray text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pre-selects companies from the database BEFORE the AI sees them.
Five matching layers: category keywords, PostgreSQL FTS, ILIKE text,
AI Insights arrays, and PKD codes. Returns rich profiles with scores
so the AI can only reference real companies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AI writes hallucinated company names at start of bullet points without
any prefix word. New pattern catches "* CompanyName to/–/specjalizuje"
and removes the fake name if it's not in the database.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AI bypasses link/bold validation by mentioning companies as plain text
like "firma Baumar" or "również Pro-Bud". New regex catches these patterns
and removes them if the company name isn't in the database.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The actual Flask route is /company/<slug>, not /firma/<slug>.
All link generation, validation, and prompt instructions now use /company/.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three layers of defense:
1. PROMPT: explicit whitelist of allowed company names + slugs
2. VALIDATOR: link slug verification (existing)
3. VALIDATOR: bold text scan — removes **FakeName** if not in DB
AI can no longer mention companies as plain/bold text to bypass link validation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- AI generates "inpi-sp-z-o-o" but real slug is "inpi" → now auto-corrected
- Fuzzy prefix matching on slugs (handles legal form suffixes)
- Name-based resolution as fallback (match link text to company name)
- Hallucinated companies: keep text, remove link (instead of deleting entirely)
- Better cleanup of artifacts ("oraz –", empty bullets)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prompt rules don't work — AI ignores them and invents company names.
Added _validate_company_references() post-processor that:
- Loads all valid company slugs from DB
- Scans every /firma/ link in AI response
- REMOVES links to companies that don't exist
- Cleans up empty list items left by removals
- Applied to BOTH send_message() and send_message_stream()
This is the ONLY reliable way to prevent hallucinated companies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove CSS filter on AI avatar — show NordaGPT robot icon as-is
- Strategic keywords (partner, inwestow, PEJ, serwerowni) → complex
- Complex queries get thinking=high for deeper analysis and less hallucination
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- NordaGPT icon (nordagpt-icon.svg) as AI avatar instead of "AI" text
- User profile photo as avatar (falls back to initial letter)
- CRITICAL: added strict rule to never hallucinate company names
- Only mention companies that exist in the provided database
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire Smart Router and Context Builder into send_message(): queries are now classified,
only needed data is loaded via build_selective_context(), and model/thinking level
are determined by the router. Falls back to full context if router is unavailable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces build_selective_context() which loads only the data categories
requested by the Smart Router (companies_all, companies_filtered:CAT,
companies_single:NAME, events, news, classifieds, forum, company_people,
registered_users, social_media, audits) instead of loading everything for
every query. Basic stats and conversation history are always included.
Company compact dict format mirrors nordabiz_chat._company_to_compact_dict()
exactly for full _query_ai() compatibility.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Classifies query complexity (simple/medium/complex), selects data categories
to load, and picks the appropriate AI model. Fast path uses keyword matching;
uncertain queries fall back to Gemini 3.1 Flash-Lite classification prompt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
15 tasks across 4 phases: user identity, smart router, streaming, memory.
Each phase independently deployable with staging-first verification.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On narrow screens the link was competing for horizontal space, splitting
the widget into two columns. Now it wraps to a full-width row below.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
microsoft-fluent.css is not loaded as a stylesheet — all styles are inline
in base.html. Moved the widget CSS to index.html's extra_css block where
it will actually be applied.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On mobile screens the "Co nowego na platformie" section took up excessive
vertical space by showing full descriptions of all starred items. Now on
<768px: descriptions hidden, only 2 items shown, reduced padding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>