Skip to content

Commit 2e3573e

Browse files
committed
feat: navigate to pdf searcg
1 parent 50b5ba9 commit 2e3573e

14 files changed

Lines changed: 271 additions & 69 deletions

File tree

apps/mcp-server/src/db.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface SearchResult {
1111
title: string | null;
1212
content: string;
1313
contentType: string;
14+
pageNumber: number | null;
1415
score: number;
1516
}
1617

@@ -39,7 +40,7 @@ export function createDb(connectionString: string) {
3940
let results: Record<string, unknown>[];
4041
if (partNumber !== undefined && contentType) {
4142
results = await sql`
42-
SELECT id, part_number, section_id, title, content, content_type,
43+
SELECT id, part_number, section_id, title, content, content_type, page_number,
4344
1 - (embedding <=> ${embeddingStr}::vector) as score
4445
FROM spec_content
4546
WHERE embedding IS NOT NULL
@@ -50,7 +51,7 @@ export function createDb(connectionString: string) {
5051
`;
5152
} else if (partNumber !== undefined) {
5253
results = await sql`
53-
SELECT id, part_number, section_id, title, content, content_type,
54+
SELECT id, part_number, section_id, title, content, content_type, page_number,
5455
1 - (embedding <=> ${embeddingStr}::vector) as score
5556
FROM spec_content
5657
WHERE embedding IS NOT NULL
@@ -60,7 +61,7 @@ export function createDb(connectionString: string) {
6061
`;
6162
} else if (contentType) {
6263
results = await sql`
63-
SELECT id, part_number, section_id, title, content, content_type,
64+
SELECT id, part_number, section_id, title, content, content_type, page_number,
6465
1 - (embedding <=> ${embeddingStr}::vector) as score
6566
FROM spec_content
6667
WHERE embedding IS NOT NULL
@@ -70,7 +71,7 @@ export function createDb(connectionString: string) {
7071
`;
7172
} else {
7273
results = await sql`
73-
SELECT id, part_number, section_id, title, content, content_type,
74+
SELECT id, part_number, section_id, title, content, content_type, page_number,
7475
1 - (embedding <=> ${embeddingStr}::vector) as score
7576
FROM spec_content
7677
WHERE embedding IS NOT NULL
@@ -86,6 +87,7 @@ export function createDb(connectionString: string) {
8687
title: r.title as string | null,
8788
content: r.content as string,
8889
contentType: r.content_type as string,
90+
pageNumber: r.page_number as number | null,
8991
score: r.score as number,
9092
}));
9193
},

apps/mcp-server/src/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,12 @@ const PART_DESCRIPTIONS: Record<number, string> = {
3030
const ALLOWED_ORIGINS = ["https://ooxml.dev", "https://www.ooxml.dev"];
3131
const DEV_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
3232

33-
function getCorsHeaders(request: Request, env: Env): Record<string, string> {
33+
function getCorsHeaders(request: Request, _env: Env): Record<string, string> {
3434
const origin = request.headers.get("Origin");
3535
if (!origin) return {};
3636

37-
// Check if origin is allowed
38-
const isProduction = !env.DATABASE_URL.includes("localhost");
39-
const allowedOrigins = isProduction ? ALLOWED_ORIGINS : [...ALLOWED_ORIGINS, ...DEV_ORIGINS];
37+
// Always allow localhost origins (safe - can only be used when running locally)
38+
const allowedOrigins = [...ALLOWED_ORIGINS, ...DEV_ORIGINS];
4039

4140
if (allowedOrigins.includes(origin)) {
4241
return {

apps/web/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# API URL for the MCP server
2+
# Local dev: http://localhost:8787
3+
# Production: https://api.ooxml.dev
4+
VITE_API_URL=http://localhost:8787

apps/web/.env.production

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Production
2+
VITE_API_URL=https://api.ooxml.dev

apps/web/src/App.tsx

Lines changed: 0 additions & 20 deletions
This file was deleted.

apps/web/src/components/SearchDialog.tsx

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { clsx } from "clsx";
2-
import { FileText, Hash, Loader2, Search } from "lucide-react";
2+
import { ExternalLink, FileText, Hash, Loader2, Search } from "lucide-react";
33
import { useEffect, useRef, useState } from "react";
44
import { Link, useNavigate } from "react-router-dom";
55
import { useSpecSearch } from "../hooks/useSpecSearch";
@@ -88,7 +88,10 @@ export function SpecSearchDialog({ open, onOpenChange }: Props) {
8888
if (activeTab === "docs") {
8989
navigate((result as (typeof localResults)[number]).url);
9090
} else {
91-
navigate((result as (typeof specResults)[number]).url);
91+
const specResult = result as (typeof specResults)[number];
92+
if (specResult.pdfUrl) {
93+
window.open(specResult.pdfUrl, "_blank");
94+
}
9295
}
9396
}
9497
}
@@ -233,9 +236,11 @@ export function SpecSearchDialog({ open, onOpenChange }: Props) {
233236

234237
{hasSpecResults &&
235238
specResults.map((result, idx) => (
236-
<Link
239+
<a
237240
key={result.id}
238-
to={result.url}
241+
href={result.pdfUrl || "#"}
242+
target="_blank"
243+
rel="noopener noreferrer"
239244
onClick={() => onOpenChange(false)}
240245
data-selected={idx === selectedIndex}
241246
onMouseEnter={() => setSelectedIndex(idx)}
@@ -246,8 +251,8 @@ export function SpecSearchDialog({ open, onOpenChange }: Props) {
246251
: "hover:bg-[var(--color-bg-secondary)]",
247252
)}
248253
>
249-
<span className="text-sm text-[var(--color-text-muted)] font-mono w-24 flex-shrink-0">
250-
{result.sectionId}
254+
<span className="text-sm text-[var(--color-text-muted)] font-mono w-20 flex-shrink-0">
255+
§ {result.sectionId}
251256
</span>
252257
<div className="min-w-0 flex-1">
253258
<div className="font-medium text-[var(--color-text-primary)]">
@@ -259,7 +264,15 @@ export function SpecSearchDialog({ open, onOpenChange }: Props) {
259264
</div>
260265
)}
261266
</div>
262-
</Link>
267+
<div className="flex items-center gap-2 flex-shrink-0">
268+
{result.pageNumber && (
269+
<span className="text-xs text-red-700 bg-red-100 px-2 py-0.5 rounded">
270+
Page {result.pageNumber}
271+
</span>
272+
)}
273+
<ExternalLink size={14} className="text-[var(--color-text-muted)]" />
274+
</div>
275+
</a>
263276
))}
264277

265278
{!isLoading && !hasSpecResults && specSearchTriggered && (

apps/web/src/hooks/useSpecSearch.ts

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,32 @@ interface MCPSearchResult {
99
title: string | null;
1010
content: string;
1111
contentType: string;
12+
pageNumber: number | null;
1213
score: number;
1314
}
1415

16+
// PDF URLs for each part (hosted on our CDN)
17+
const PDF_URLS: Record<number, string> = {
18+
1: "https://cdn.ooxml.dev/ecma-376/part1.pdf",
19+
2: "https://cdn.ooxml.dev/ecma-376/part2.pdf",
20+
3: "https://cdn.ooxml.dev/ecma-376/part3.pdf",
21+
4: "https://cdn.ooxml.dev/ecma-376/part4.pdf",
22+
};
23+
1524
interface MCPSearchResponse {
1625
query: string;
1726
results: MCPSearchResult[];
1827
}
1928

20-
// Extended result type with section ID for display
29+
// Extended result type with section ID and PDF link
2130
export interface SpecSearchResult {
2231
id: string;
23-
url: string;
2432
sectionId: string;
2533
title: string;
2634
description?: string;
35+
partNumber: number;
36+
pageNumber: number | null;
37+
pdfUrl: string | null;
2738
}
2839

2940
// Local docs search result
@@ -117,20 +128,28 @@ export function useSpecSearch() {
117128
setSpecSearchTriggered(true);
118129

119130
try {
120-
const res = await fetch("https://api.ooxml.dev/search", {
131+
const res = await fetch(`${import.meta.env.VITE_API_URL}/search`, {
121132
method: "POST",
122133
headers: { "Content-Type": "application/json" },
123134
body: JSON.stringify({ query, limit: 10 }),
124135
});
125136
const data: MCPSearchResponse = await res.json();
126137

127-
const transformed: SpecSearchResult[] = data.results.map((r) => ({
128-
id: `spec-${r.id}`,
129-
url: r.sectionId ? `/docs/${r.sectionId}` : "#",
130-
sectionId: r.sectionId || "",
131-
title: r.title || r.content.slice(0, 60),
132-
description: r.title ? r.content.slice(0, 80) : undefined,
133-
}));
138+
const transformed: SpecSearchResult[] = data.results.map((r) => {
139+
const pdfBase = PDF_URLS[r.partNumber];
140+
const pdfUrl =
141+
pdfBase && r.pageNumber ? `${pdfBase}#page=${r.pageNumber}` : pdfBase || null;
142+
143+
return {
144+
id: `spec-${r.id}`,
145+
sectionId: r.sectionId || "",
146+
title: r.title || r.content.slice(0, 60),
147+
description: r.title ? r.content.slice(0, 80) : undefined,
148+
partNumber: r.partNumber,
149+
pageNumber: r.pageNumber,
150+
pdfUrl,
151+
};
152+
});
134153

135154
setSpecResults(transformed);
136155
} catch (err) {

apps/web/src/pages/Mcp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useState } from "react";
22
import { Link } from "react-router-dom";
33
import { Navbar } from "../components/Navbar";
44

5-
const MCP_ENDPOINT = "https://api.ooxml.dev/mcp";
5+
const MCP_ENDPOINT = `${import.meta.env.VITE_API_URL}/mcp`;
66
const CLAUDE_COMMAND = `claude mcp add --transport http ecma-spec ${MCP_ENDPOINT}`;
77

88
const TOOLS = [

db/schema.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CREATE TABLE spec_content (
1111
title TEXT,
1212
content TEXT NOT NULL,
1313
content_type TEXT DEFAULT 'text',
14+
page_number INT,
1415
embedding vector(1024),
1516
created_at TIMESTAMPTZ DEFAULT NOW()
1617
);

packages/shared/src/db/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@ export function createDbClient(connectionString: string) {
1616
// Insert content
1717
async insert(content: Omit<SpecContent, "id">) {
1818
const [result] = await sql<[{ id: number }]>`
19-
INSERT INTO spec_content (part_number, section_id, title, content, content_type, embedding)
19+
INSERT INTO spec_content (part_number, section_id, title, content, content_type, page_number, embedding)
2020
VALUES (
2121
${content.partNumber},
2222
${content.sectionId},
2323
${content.title},
2424
${content.content},
2525
${content.contentType},
26+
${content.pageNumber},
2627
${content.embedding ? `[${content.embedding.join(",")}]` : null}
2728
)
2829
RETURNING id
@@ -38,6 +39,7 @@ export function createDbClient(connectionString: string) {
3839
title: item.title,
3940
content: item.content,
4041
content_type: item.contentType,
42+
page_number: item.pageNumber,
4143
embedding: item.embedding ? `[${item.embedding.join(",")}]` : null,
4244
}));
4345

@@ -73,11 +75,12 @@ export function createDbClient(connectionString: string) {
7375
title: string | null;
7476
content: string;
7577
content_type: string;
78+
page_number: number | null;
7679
score: number;
7780
}>
7881
>`
7982
SELECT
80-
id, part_number, section_id, title, content, content_type,
83+
id, part_number, section_id, title, content, content_type, page_number,
8184
1 - (embedding <=> ${embeddingStr}::vector) as score
8285
FROM spec_content
8386
WHERE embedding IS NOT NULL
@@ -94,6 +97,7 @@ export function createDbClient(connectionString: string) {
9497
title: r.title,
9598
content: r.content,
9699
contentType: r.content_type,
100+
pageNumber: r.page_number,
97101
score: r.score,
98102
}));
99103
},
@@ -108,9 +112,10 @@ export function createDbClient(connectionString: string) {
108112
title: string | null;
109113
content: string;
110114
content_type: string;
115+
page_number: number | null;
111116
}>
112117
>`
113-
SELECT id, part_number, section_id, title, content, content_type
118+
SELECT id, part_number, section_id, title, content, content_type, page_number
114119
FROM spec_content
115120
WHERE part_number = ${partNumber} AND section_id = ${sectionId}
116121
ORDER BY id
@@ -123,6 +128,7 @@ export function createDbClient(connectionString: string) {
123128
title: r.title,
124129
content: r.content,
125130
contentType: r.content_type,
131+
pageNumber: r.page_number,
126132
}));
127133
},
128134

0 commit comments

Comments
 (0)