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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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?://`
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user