feat(playwright): integrate Playwright for end-to-end testing and update .gitignore

- Added Playwright as a development dependency for end-to-end testing.
- Updated package.json to include test scripts for Playwright.
- Enhanced .gitignore to exclude Playwright test results and cache files.
- Improved content extraction logic in various components to handle new content types.

Made-with: Cursor
This commit is contained in:
Dorian
2026-03-02 22:02:19 +00:00
parent 5414060225
commit 43ca3a7837
23 changed files with 653 additions and 13 deletions
+9
View File
@@ -80,3 +80,12 @@ interface RendererDefinition {
- Panel surfaces may lazy-load heavy libraries (CodeMirror, pdf.js, etc.)
- Never block the chat scroll with renderer loading
- Use skeleton/placeholder while panel content loads
## Content Type Expert Rules
For extraction, parsing, and surfacing logic, see:
- `20-content-films.mdc` — Films
- `21-content-songs.mdc` — Songs (includes looksLikeSong blocklist)
- `22-content-podcasts.mdc` — Podcasts (includes looksLikePodcast)
- `23-content-news.mdc` — News + RSS, ArticleDetail security
- `24-content-websites.mdc` — Websites vs News, overlay
- `25-content-magazine.mdc` — Magazine/Brief parsing, hero, meme
+30
View File
@@ -0,0 +1,30 @@
---
description: Expert rules for Film content extraction, display, and surfacing
globs: "**/useContentPanel.ts,**/FilmCard.vue,**/FilmGrid.vue,**/FilmDetail.vue,**/mocks/films*"
alwaysApply: false
---
# Films Content Surface
## Extraction Patterns
- **Tagged**: `[[film:f123]]` or `[[film:123]]` → resolved from mock library
- **External**: `[[film_ext:Title|YYYY|Director]]` → create external film with fallback poster
## Edge Cases
- `normalizeFilmId`: `f123` and `123` both become `f123`
- Duplicate prevention: key by `title|year` for externals
- Empty/malformed: skip if title < 2 chars, year invalid
- Poster: use `generatePosterFallback(title, year)` for externals
## Strip Rules
- `stripFilmTags` removes `[[film:...]]` and `[[film_ext:...]]` before displaying text
- Preserve `\n{3,}` → `\n\n` to avoid excessive whitespace
## Display
- FilmCard: poster, title, year, director
- FilmDetail: full metadata, sources, cast
- Panel: grid of FilmCards, click opens FilmDetail in panel
+31
View File
@@ -0,0 +1,31 @@
---
description: Expert rules for Song content extraction, display, and surfacing
globs: "**/useContentPanel.ts,**/SongCard.vue,**/SongGrid.vue,**/SongDetail.vue,**/mocks/songs*"
alwaysApply: false
---
# Songs Content Surface
## Extraction Priority
1. Tagged: `[[song:s123]]` or `[[song_ext:Title|Artist|YYYY]]`
2. Library match: title + artist within 120 chars
3. Patterns: `"Title" by Artist`, `Title Artist`, `**Title** by Artist`
## looksLikeSong Rejection
Reject when title/artist contains: news phrases, "BIP", "protocol", "web search", "mailing list", "training cutoff", etc. See `looksLikeSong()` blocklist.
- Max length: title 55 chars, artist 40 chars
## Edge Cases
- If `extractFilmIds` or `extractPodcastIds` found → return [] (don't mix film/podcast with song patterns)
- If `isNewsLikeResponse` → return [] (news bullets often look like "X Y")
- Skip if title/artist is 4-digit year
- Skip if contains `[[film` or `[[song` tags
- Dedupe by `title|artist` lowercase
## Strip Rules
- `stripSongTags` removes song tags before displaying text
+26
View File
@@ -0,0 +1,26 @@
---
description: Expert rules for Podcast content extraction, display, and surfacing
globs: "**/useContentPanel.ts,**/PodcastCard.vue,**/PodcastGrid.vue,**/PodcastDetail.vue,**/mocks/podcasts*"
alwaysApply: false
---
# Podcasts Content Surface
## Extraction Patterns
- **Tagged**: `[[podcast:p123]]` or `[[podcast_ext:Title|Host|YYYY]]`
- No pattern fallback (unlike songs) — only tags
## Edge Cases
- Duplicate prevention: key by `title|host` lowercase
- Empty: skip if title or host < 2 chars
- Year optional in external format
## looksLikePodcast (when added)
Reject when title/host looks like: news source names, documentation sites, "Bitcoin Mailing List", etc. — same philosophy as `looksLikeSong`.
## Strip Rules
- `stripPodcastTags` removes podcast tags before displaying text
+47
View File
@@ -0,0 +1,47 @@
---
description: Expert rules for News content extraction, merge, and surfacing
globs: "**/useContentPanel.ts,**/useRssFetch.ts,**/NewsGrid.vue,**/ArticleDetail.vue,**/vite-rss*"
alwaysApply: false
---
# News Content Surface
## Sources
1. **Web search**: `message.webResults` from AI (with imgSrc, content)
2. **RSS**: Fetched from website URLs only when `newsContext` is true
## newsContext
- `isNewsQuery(userQuery)` — "news", "latest", "what's happening", "what are people saying", etc.
- `isNewsLikeResponse(text)` — "for instant news", "check these sources", "access to web search", etc.
## Merge Rules
- `mergeNewsResults(web, rss)` — dedupe by URL (normalized: lowercase, no trailing slash)
- Web results take precedence when URL collision
## RSS Fetch Guard
- **Only fetch RSS when `newsContext` is true and `mergedWebsites.length > 0`** — avoid surfacing irrelevant RSS from docs/resource links when user asked "websites"
- Max 8 URLs, 15 articles total, 5 sites tried
- Timeout: 15s client, 5s per feed server-side
## Display
- NewsGrid (variant=news): articles open in **ArticleDetail** (in-panel)
- Relevance sort when `query` provided
- Search filter by title, content, url
- imgSrc: validate with `isSafeImgUrl` (https only)
## Known Limitations
- **RSS language**: Feeds return whatever the site publishes; no query/language filtering — may surface non-English articles
- **RSS relevance**: No semantic filtering; articles are shown as published
## ArticleDetail Security
- `sanitizeHtml`: allow only safe tags (p, br, a, strong, em, ul, ol, li, blockquote, h1-h4)
- Strip script, style, iframe, object, embed
- Links: `href` must be `https?://`, reject `javascript:`
- Images: `src` must be `https?://`
+32
View File
@@ -0,0 +1,32 @@
---
description: Expert rules for Websites content extraction and surfacing
globs: "**/useContentPanel.ts,**/NewsGrid.vue,**/articleOverlay*"
alwaysApply: false
---
# Websites Content Surface
## Extraction
1. **Markdown links**: `[Title](https://...)` — extract all with `extractMarkdownLinks`
2. **Bold domains**: `**Name** (domain.tld)` — extract with `extractBoldDomainLinks`
3. Merge with `mergeNewsResults` (dedupe by URL)
## URLs Validation
- Scheme: `https?://` only
- `new URL(raw)` must not throw
- Min length: title 2, url 10 chars
- Normalize for dedupe: lowercase, no trailing slash
## Display
- NewsGrid (variant=websites): card with favicon/globe icon
- Click → **overlay iframe** (not ArticleDetail)
- Use `articleOverlayStore.open(url, title, undefined, imgSrc)`
## Distinction from News
- News = articles (web search + RSS) → ArticleDetail in panel
- Websites = plain links from response → overlay iframe
- Same NewsGrid component, different `variant` and click handler
+42
View File
@@ -0,0 +1,42 @@
---
description: Expert rules for Magazine/Brief content extraction and surfacing
globs: "**/useContentPanel.ts,**/MagazineGrid.vue"
alwaysApply: false
---
# Magazine Content Surface
## Detection
- `hasMagazine` = sections ≥ 1 AND (newsQuery OR newsLikeResponse OR context keywords)
- Context keywords: sentiment, bearish, bull case, macro, %, BTC, bitcoin, BIP, protocol, debate, what's happening
## Section Extraction Order
1. `## Heading` blocks — content until next ## or **Section**
2. `**Pro/Anti camp**` blocks with emoji
3. Bullets: `- **Title**: Content` or `- **Title** — Content` (em/en dash)
4. Attributed: `- **Name** (Role) description`
5. Intro paragraph (before first ##)
6. "Key takeaway" / "This is being called..."
7. "For deeper analysis" / further reading
## Section Rules
- Min: title 2 chars, content 15 chars
- Max content: 2000 chars per section
- Dedupe by title prefix (first 50 chars)
- Skip bullets already inside ## blocks (`blockContents`)
- `addSection` extracts: url, author, imageUrl from content
## Hero Image
1. First markdown image in text
2. First `.jpg|.png|.gif|.webp` URL
3. `webResults[0]?.imgSrc`
4. Picsum fallback seeded by query
## Format & Security
- `formatContent`: escape `&<>`, preserve `**bold**` as `<strong>`, `\n\n` → `</p><p>`
- Meme: imgflip URLs, contextual by topic (bearish, bull, Bitcoin, macro)