diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c44673e..705d94d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,5 +29,11 @@ jobs: - name: Test run: npm test + - name: Install Playwright browser + run: npx playwright install chromium + + - name: E2E + run: npm run test:e2e + - name: Build run: npm run build diff --git a/.gitignore b/.gitignore index a26a3cb..454bd12 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ dist/ coverage/ AI_REVIEW_BRIEF.md .claude/ +test-results/ +playwright-report/ diff --git a/docs/HAGGLY_V2_AGENT_PLAN.md b/docs/HAGGLY_V2_AGENT_PLAN.md index f35acce..eef3515 100644 --- a/docs/HAGGLY_V2_AGENT_PLAN.md +++ b/docs/HAGGLY_V2_AGENT_PLAN.md @@ -6,7 +6,9 @@ Last updated: 2026-04-28 Haggly v2 should launch as a no-login app first. -The MVP is a green-leaning, no-paywall AI negotiation chat app with a lightweight local dashboard. "Green" means visual direction and brand feel, not a sustainability claim. +The MVP is a green-leaning, no-paywall agentic AI negotiation workspace with a lightweight local dashboard. "Green" means visual direction and brand feel, not a sustainability claim. + +Haggly v2 is its own product direction. It should not behave like the current `www.haggly.io` v1 static site or a simple script generator. The v2 assistant should act like a negotiation agent: diagnose leverage, identify missing context, ask useful follow-up questions, recommend the next move, and draft wording only when drafting is actually the next best step. Do not build account login, billing, Stripe, or a hidden member dashboard for the first v2 launch. Those can come after the chat experience proves useful. @@ -17,9 +19,10 @@ The first useful version should let a visitor: 1. Open Haggly without logging in. 2. Choose buyer or seller mode. 3. Describe the negotiation. -4. Chat with an AI negotiation assistant. -5. Copy suggested responses. -6. See recent negotiation sessions in a local dashboard. +4. Chat with a Haggly negotiation agent. +5. Get a read on leverage, missing context, and next move. +6. Copy suggested wording when the agent decides drafting is useful. +7. See recent negotiation sessions in a local dashboard. Local browser storage is acceptable for MVP. If a user clears browser data, their history disappears. @@ -58,6 +61,12 @@ Main current problems: - `npm run lint` is defined but ESLint is not installed/configured. - Current styling is purple/blue, not the intended v2 direction. +Important product boundary: + +- v1/current site: static, content-heavy, script/generator lineage. +- v2/Haggly agent: interactive negotiation agent with memory, context gathering, and strategy-first behavior. +- Use v1 and the old markdown scripts as source material only. Do not make v2 a wrapper around canned scripts. + ## Build Strategy Use vertical slices. Do not build a huge frontend and huge backend separately. @@ -267,13 +276,14 @@ Tasks: - Add message composer. - Add assistant/user message bubbles. - Add loading state. -- Use mock AI responses first. +- Use mock Haggly-agent responses first. +- The mock assistant should diagnose and ask follow-up questions; it should not simply return canned Friendly/Firm/Casual scripts. - Preserve copy-to-clipboard behavior. Acceptance criteria: - User can send a message. -- Assistant returns a mock response. +- Assistant returns mock agent guidance with leverage read, missing context, and next move. - Messages stay visible in the current session. - No backend or API key required. diff --git a/package-lock.json b/package-lock.json index 5b04349..5364eb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^6.0.1", @@ -626,6 +627,22 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.17", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", @@ -2742,6 +2759,53 @@ "node": ">= 6" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.12", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", diff --git a/package.json b/package.json index 87b8eae..1f941b8 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "vite build", "lint": "eslint . --report-unused-disable-directives --max-warnings 0", "test": "vitest run", + "test:e2e": "playwright test", "preview": "vite preview" }, "dependencies": { @@ -17,6 +18,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.59.1", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^6.0.1", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..0c439cf --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,24 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 30_000, + expect: { + timeout: 5_000, + }, + use: { + baseURL: 'http://127.0.0.1:3000', + trace: 'on-first-retry', + }, + webServer: { + command: 'npm run dev -- --host 127.0.0.1 --port 3000 --strictPort', + url: 'http://127.0.0.1:3000', + reuseExistingServer: !process.env.CI, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/src/App.jsx b/src/App.jsx index cbca8d8..b768183 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,57 +2,33 @@ import { useState } from 'react' import LandingPage from './pages/LandingPage' import BuyerPage from './pages/BuyerPage' import SellerPage from './pages/SellerPage' -import { generateMessages } from './utils/messageGenerator' function App() { const [userType, setUserType] = useState(null) // 'buyer' or 'seller' - const [responses, setResponses] = useState(null) const [toast, setToast] = useState(null) - const [isGenerating, setIsGenerating] = useState(false) - - const handleGenerate = (formData) => { - setIsGenerating(true) - - // Small delay for visual feedback - setTimeout(() => { - const messages = generateMessages(formData) - setResponses(messages) - setIsGenerating(false) - - // Scroll to results on mobile - setTimeout(() => { - document.getElementById('results')?.scrollIntoView({ behavior: 'smooth' }) - }, 100) - }, 300) - } const showToast = (message) => { setToast(message) setTimeout(() => setToast(null), 2000) } - const handleReset = () => { - setResponses(null) - setUserType(null) - window.scrollTo({ top: 0, behavior: 'smooth' }) - } - if (!userType) { return } if (userType === 'buyer') { - return setUserType(null)} /> + return ( + setUserType(null)} + onCopy={showToast} + /> + ) } return ( setUserType(null)} - onGenerate={handleGenerate} - onReset={handleReset} onCopy={showToast} /> ) diff --git a/src/features/chat/ChatComposer.jsx b/src/features/chat/ChatComposer.jsx new file mode 100644 index 0000000..653a9b4 --- /dev/null +++ b/src/features/chat/ChatComposer.jsx @@ -0,0 +1,44 @@ +import { Send } from 'lucide-react' +import { useState } from 'react' +import Button from '../../components/ui/Button' + +function ChatComposer({ disabled, onSend }) { + const [draft, setDraft] = useState('') + + const handleSubmit = (event) => { + event.preventDefault() + const message = draft.trim() + + if (!message) { + return + } + + onSend(message) + setDraft('') + } + + return ( +
+
+ +