Insert links to knowledge base from admin chat, and use vector search#29
Insert links to knowledge base from admin chat, and use vector search#29
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Review Summary by QodoIntegrate vector search for inbox knowledge and add article link widget support
WalkthroughsDescription• Implement vector search for inbox knowledge picker using embeddings • Change article link format to article:<id> for widget integration • Add article link detection in markdown with data-article-id attribute • Enable widget to handle article links as in-widget navigation Diagramflowchart LR
A["Inbox Knowledge Search"] -->|"uses vector embeddings"| B["searchWithEmbeddings Action"]
B -->|"returns results"| C["useInboxConvex Hook"]
C -->|"displays in picker"| D["InboxThreadPane"]
D -->|"inserts as article:id"| E["Message Content"]
E -->|"parsed by shared markdown"| F["data-article-id attribute"]
F -->|"click handler"| G["Widget onSelectArticle"]
G -->|"opens article view"| H["Widget Help Center"]
File Changes1. packages/convex/convex/knowledge.ts
|
Code Review by Qodo
1.
|
CI Feedback 🧐A test triggered by this PR failed. Here is an AI-generated analysis of the failure:
|
There was a problem hiding this comment.
Pull request overview
This PR updates the inbox knowledge workflow to (1) use embedding-based vector search for more relevant results, and (2) insert knowledge-base article references as article:<id> links so the widget can open articles in-widget via data-article-id click handling. It also updates shared markdown sanitization to recognize article: links and includes related widget styling/spec documentation updates.
Changes:
- Add
knowledge:searchWithEmbeddingsConvex action using vector search + embeddings and wire it into the inbox hook. - Standardize article link insertion to
[title](article:<articleId>)and update shared markdown sanitization to emitdata-article-idfor widget navigation. - Add widget-side click handling for rendered message content and update widget styles/spec docs to support the new behavior.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/web-shared/src/markdown.ts | Allow article: link protocol, emit data-article-id, and adjust link hardening/sanitization behavior. |
| packages/web-shared/src/markdown.test.ts | Add tests validating article link detection and sanitization behavior. |
| packages/convex/convex/knowledge.ts | Add searchWithEmbeddings action using embedding generation + vector search and enrich results. |
| apps/web/src/app/inbox/hooks/useInboxConvex.ts | Replace reactive query-based knowledge search with action-based search managed via useEffect/useState. |
| apps/web/src/app/inbox/page.tsx | Update knowledge insertion to use article:<id> link format (now also for internalArticle). |
| apps/web/src/app/inbox/InboxThreadPane.tsx | Update knowledge picker UI actions to focus on inserting links (diff currently includes commented-out JSX). |
| apps/widget/src/components/conversationView/MessageList.tsx | Add click handler to open widget article view when clicking an element with data-article-id. |
| apps/widget/src/styles.css | Add .opencom-article-link styling and reformat transitions/gradients for readability. |
| openspec/specs/shared-markdown-rendering-sanitization/spec.md | Document requirement for detecting article: links and emitting navigation metadata. |
| openspec/specs/inbox-knowledge-vector-search/spec.md | New spec describing inbox vector-search requirements and workspace scoping. |
| openspec/specs/inbox-knowledge-insertion/spec.md | Update spec to define article link insertion format and behavior. |
| openspec/specs/ai-help-center-linked-sources/spec.md | Add requirement for consistent article link format with AI sources. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/tasks.md | Archive task list for this change set. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/shared-markdown-rendering-sanitization/spec.md | Archived spec delta for markdown article link detection. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-vector-search/spec.md | Archived spec delta for inbox vector search. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-insertion/spec.md | Archived spec delta for insertion behavior changes. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/ai-help-center-linked-sources/spec.md | Archived spec delta for consistent article link formatting. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/proposal.md | Archive proposal for the overall change. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/design.md | Archive design notes including risks/trade-offs. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/.openspec.yaml | Archive metadata for the change set. |
Comments suppressed due to low confidence (1)
packages/web-shared/src/markdown.ts:146
- Allowing the
articleprotocol inhasDisallowedAbsoluteProtocolaffects both links and images. Because this helper is also used for<img src>, markdown likewill now keep animgwith anarticle:src, which is likely unintended. Consider keepingarticle:allowed only for anchors (special-case in the<a>branch) while continuing to restrict<img>to http/https only.
function hasDisallowedAbsoluteProtocol(rawUrl: string): boolean {
const match = rawUrl.trim().match(/^([a-z0-9+.-]+):/i);
if (!match) {
return false;
}
const protocol = match[1].toLowerCase();
return protocol !== "http" && protocol !== "https" && protocol !== "article";
}
function isArticleLink(href: string): boolean {
return href.trim().toLowerCase().startsWith("article:");
}
function extractArticleId(href: string): string | null {
const match = href.trim().match(/^article:([a-zA-Z0-9]+)$/i);
return match ? match[1] : null;
}
function enforceSafeLinksAndMedia(html: string, options: ResolvedParseMarkdownOptions): string {
const container = document.createElement("div");
container.innerHTML = html;
container.querySelectorAll("a").forEach((anchor) => {
const href = anchor.getAttribute("href");
if (!href || hasBlockedProtocol(href)) {
anchor.removeAttribute("href");
anchor.removeAttribute("target");
anchor.removeAttribute("rel");
return;
}
if (isArticleLink(href)) {
const articleId = extractArticleId(href);
if (articleId) {
anchor.setAttribute("data-article-id", articleId);
anchor.setAttribute("class", "opencom-article-link");
anchor.removeAttribute("href");
anchor.removeAttribute("target");
anchor.removeAttribute("rel");
} else {
anchor.removeAttribute("href");
anchor.removeAttribute("target");
anchor.removeAttribute("rel");
}
return;
}
if (hasDisallowedAbsoluteProtocol(href)) {
anchor.removeAttribute("href");
anchor.removeAttribute("target");
anchor.removeAttribute("rel");
return;
}
if (options.linkTarget === null) {
anchor.removeAttribute("target");
} else {
anchor.setAttribute("target", options.linkTarget);
}
if (options.linkRel === null) {
anchor.removeAttribute("rel");
} else {
anchor.setAttribute("rel", options.linkRel);
}
});
container.querySelectorAll("img").forEach((image) => {
const src = image.getAttribute("src");
if (!src || hasBlockedProtocol(src) || hasDisallowedAbsoluteProtocol(src)) {
image.removeAttribute("src");
}
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (isArticleLink(href)) { | ||
| const articleId = extractArticleId(href); | ||
| if (articleId) { | ||
| anchor.setAttribute("data-article-id", articleId); | ||
| anchor.setAttribute("class", "opencom-article-link"); | ||
| anchor.removeAttribute("href"); | ||
| anchor.removeAttribute("target"); | ||
| anchor.removeAttribute("rel"); | ||
| } else { | ||
| anchor.removeAttribute("href"); | ||
| anchor.removeAttribute("target"); | ||
| anchor.removeAttribute("rel"); | ||
| } | ||
| return; |
| function getShallowRunQuery(ctx: { runQuery: unknown }) { | ||
| return ctx.runQuery as unknown as <Args extends Record<string, unknown>, Return>( | ||
| queryRef: EmbeddingQueryRef<Args, Return>, | ||
| queryArgs: Args | ||
| ) => Promise<Return>; | ||
| } |
There was a problem hiding this comment.
2. getshallowrunquery chained cast 📘 Rule violation ✓ Correctness
The new helper getShallowRunQuery uses a chained escape cast (as unknown as ...) without any justification comment. This expands unsafe type-escape usage in runtime code beyond the allowed documented exceptions.
Agent Prompt
## Issue description
`getShallowRunQuery` introduces a chained type-escape cast (`as unknown as ...`) with no documentation. The rule requires these casts to be minimal, hotspot-local, and documented.
## Issue Context
This helper is used to call `ctx.runQuery(...)` inside `searchWithEmbeddings`. Ideally, type `ctx` using the correct Convex action context type (or a properly typed `runQuery` signature) to eliminate the cast; if a cast is unavoidable, add a `NOTE:` comment explaining why and how it can be removed.
## Fix Focus Areas
- packages/convex/convex/knowledge.ts[271-276]
- packages/convex/convex/knowledge.ts[282-299]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Pull request overview
This PR updates the inbox + widget knowledge-link workflow by switching inbox knowledge search to a vector/embeddings-based Convex action, standardizing inserted article links to an article:<id> format for widget navigation, and extending the shared markdown sanitizer to emit data-article-id metadata for those links.
Changes:
- Add
knowledge:searchWithEmbeddings(Convex action) to power inbox semantic knowledge search usingvectorSearch+ embeddings. - Change inbox “insert knowledge” behavior to insert article links as
[title](article:<articleId>)and update shared markdown sanitization to preserve/annotatearticle:links. - Update widget message rendering to intercept clicks on
[data-article-id]links and add styling for.opencom-article-link.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| security/dependency-audit-allowlist.json | Adds allowlist entries for undici advisories (dev-only transitive deps). |
| packages/web-shared/src/markdown.ts | Allows article: protocol through sanitization and annotates article links with data-article-id. |
| packages/web-shared/src/markdown.test.ts | Adds tests for article: link handling + sanitization behavior. |
| packages/convex/tests/runtimeTypeHardeningGuard.test.ts | Extends guard coverage to include the new knowledge vector-search implementation. |
| packages/convex/convex/knowledge.ts | Introduces searchWithEmbeddings action using embeddings + vector search and typed refs. |
| openspec/specs/shared-markdown-rendering-sanitization/spec.md | Documents new requirement for article-link metadata in shared markdown rendering. |
| openspec/specs/inbox-knowledge-vector-search/spec.md | New spec for inbox vector search behavior + workspace scoping. |
| openspec/specs/inbox-knowledge-insertion/spec.md | Updates insertion requirements to support [title](article:<articleId>) links. |
| openspec/specs/ai-help-center-linked-sources/spec.md | Aligns agent-inserted article links with AI sources’ ID-based linking. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/tasks.md | Archived task checklist for the change bundle. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/shared-markdown-rendering-sanitization/spec.md | Archived spec delta for shared markdown updates. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-vector-search/spec.md | Archived spec delta for inbox vector search. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/inbox-knowledge-insertion/spec.md | Archived spec delta for inbox insertion updates. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/specs/ai-help-center-linked-sources/spec.md | Archived spec delta for consistent link formatting. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/proposal.md | Archived proposal describing rationale and scope. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/design.md | Archived design notes and tradeoffs for article links + vector search. |
| openspec/changes/archive/2026-03-13-article-link-widget-integration/.openspec.yaml | Adds archived change metadata. |
| apps/widget/src/styles.css | Adds .opencom-article-link styling and reformats transitions/gradients. |
| apps/widget/src/components/conversationView/MessageList.tsx | Adds click interception for data-article-id links to open articles in-widget. |
| apps/web/src/app/inbox/page.tsx | Updates insertion format for article links to use article:<id>. |
| apps/web/src/app/inbox/hooks/useInboxConvex.ts | Switches knowledge search from query to action and manages results via state/effect. |
| apps/web/src/app/inbox/InboxThreadPane.tsx | Adjusts knowledge picker UI to focus on inserting links (button behavior changes). |
| AGENTS.md | Adds guidance to run pnpm ci:check before opening a PR. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return ( | ||
| <Button size="sm" variant="outline" onClick={() => onInsertKnowledgeContent(item)}> | ||
| Insert Content | ||
| <Button | ||
| size="sm" | ||
| variant="outline" | ||
| onClick={() => onInsertKnowledgeContent(item, "link")} | ||
| > | ||
| <Link className="mr-1 h-3.5 w-3.5" /> | ||
| Insert Link | ||
| </Button> |
| if (item.type === "snippet") { | ||
| setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}${item.content}`); | ||
| setLastInsertedSnippetId(item.id as Id<"snippets">); | ||
| } else if (action === "link" && item.type === "article" && item.slug) { | ||
| setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}[${item.title}](/help/${item.slug})`); | ||
| } else if (action === "link" && item.type === "article") { | ||
| setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}[${item.title}](article:${item.id})`); | ||
| } else { | ||
| setInputValue((prev) => `${prev}${prev ? "\n\n" : ""}${item.content}`); | ||
| } |
| const handleMessageClick = (event: React.MouseEvent<HTMLDivElement>) => { | ||
| const target = event.target as HTMLElement; | ||
| const articleLink = target.closest("[data-article-id]"); | ||
| if (articleLink) { | ||
| event.preventDefault(); | ||
| event.stopPropagation(); | ||
| const articleId = articleLink.getAttribute("data-article-id"); | ||
| if (articleId) { | ||
| onSelectArticle(articleId as Id<"articles">); | ||
| } |
This pull request introduces improvements to the knowledge search experience in the inbox, updates how knowledge links are inserted, and refines the widget's message interaction and UI styling. The most significant changes are grouped below:
Knowledge Search and Insertion Improvements:
knowledge:searchWithEmbeddings), enabling more dynamic and responsive search results inuseInboxConvexand updating the hook to manage knowledge search state withuseEffectanduseState. ([[1]](https://github.com/opencom-org/opencom/pull/29/files#diff-8c41c9000ef8f232062798bd1c7a03e6f82c2b876341179c290b4b99e7524935L147-R149),[[2]](https://github.com/opencom-org/opencom/pull/29/files#diff-8c41c9000ef8f232062798bd1c7a03e6f82c2b876341179c290b4b99e7524935R181-R211),[[3]](https://github.com/opencom-org/opencom/pull/29/files#diff-8c41c9000ef8f232062798bd1c7a03e6f82c2b876341179c290b4b99e7524935L191-R225),[[4]](https://github.com/opencom-org/opencom/pull/29/files#diff-8c41c9000ef8f232062798bd1c7a03e6f82c2b876341179c290b4b99e7524935R3))[title](article:id)for botharticleandinternalArticletypes, ensuring consistent referencing. ([apps/web/src/app/inbox/page.tsxL384-R388](https://github.com/opencom-org/opencom/pull/29/files#diff-f05cfd8fbff7ba91780a2726674e49ff587e720d6fc118df8ff30d3ef1eaac90L384-R388))InboxThreadPaneto focus on inserting article links instead of full content, and adjusted the button behavior accordingly. ([apps/web/src/app/inbox/InboxThreadPane.tsxL210-R228](https://github.com/opencom-org/opencom/pull/29/files#diff-83253b7f536695a1c89d914afbc128313d6f6d850df460f17453f60cc920f181L210-R228))Widget Message Interaction:
data-article-id) triggers the article selection callback, improving navigation and interactivity. ([[1]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10R74-R86),[[2]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10R146))UI and Styling Enhancements:
[[1]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L37-R43),[[2]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L77-R86),[[3]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L156-R167),[[4]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L254-R267),[[5]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L344-R361),[[6]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L492-R512),[[7]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L518-R551),[[8]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L541-R569),[[9]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L802-R834),[[10]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L843-R876),[[11]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L936-R982),[[12]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L966-R1007),[[13]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L990-R1033),[[14]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1200-R1245),[[15]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1250-R1297),[[16]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1326-R1375),[[17]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1401-R1452),[[18]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1685-R1739),[[19]](https://github.com/opencom-org/opencom/pull/29/files#diff-5f52cc0c97ac6a7092a4a335af1cc692b268fe10a7085b7ba1001909e1d88053L1804-R1860))Minor Code Quality and Formatting:
[[1]](https://github.com/opencom-org/opencom/pull/29/files#diff-f05cfd8fbff7ba91780a2726674e49ff587e720d6fc118df8ff30d3ef1eaac90L286-R297),[[2]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10L105-R120),[[3]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10L118-R136),[[4]](https://github.com/opencom-org/opencom/pull/29/files#diff-ce3c188b3ef7a87f8c7abba79a5aeb9a3a08315a5e68d668b530daf606e6ca10L153-R174))These changes collectively enhance the user and developer experience around knowledge search, insertion, and message interaction in the inbox and widget.