diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd295b8..d8761cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,16 +7,18 @@ on: pull_request: permissions: - contents: read + contents: write jobs: test: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 35 steps: - name: Check out repository uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - name: Set up Java 8 uses: actions/setup-java@v4 @@ -28,7 +30,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: '20.19.0' + node-version: '22' cache: npm cache-dependency-path: frontend/package.json @@ -49,3 +51,97 @@ jobs: - name: Validate Docker Compose run: docker compose config --quiet + + - name: Start the full playground stack + run: docker compose up --build --detach + + - name: Wait for browser and API readiness + run: | + for attempt in $(seq 1 90); do + if curl --fail --silent --show-error http://127.0.0.1:3000/api/health > /tmp/playground-health.json; then + cat /tmp/playground-health.json + exit 0 + fi + sleep 2 + done + echo 'Playground did not become ready.' >&2 + docker compose logs --no-color + exit 1 + + - name: Install Playwright Chromium + working-directory: frontend + run: npx playwright install --with-deps chromium + + - name: Run browser acceptance test and capture screenshot + working-directory: frontend + run: npm run e2e + + - name: Commit accepted screenshot and README showcase to the PR branch + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + env: + HEAD_REF: ${{ github.event.pull_request.head.ref }} + run: | + python3 - <<'PY' + from pathlib import Path + + updates = [ + ( + Path('README.md'), + '## Why Java-RAG?', + ''' + ## Visual Playground + + ![Java-RAG visual playground with knowledge base, grounded chat, and retrieval evidence](docs/assets/playground.png) + + This screenshot is generated by the Playwright acceptance test after Docker Compose starts the real application, the browser loads the sample document, asks a grounded question, and verifies the matching retrieval evidence.''', + ), + ( + Path('README_ch.md'), + '## 为什么值得关注?', + ''' + ## 可视化 Playground + + ![Java-RAG 可视化知识库、证据化问答与检索证据界面](docs/assets/playground.png) + + 这张截图由 Playwright 验收测试自动生成:Docker Compose 启动真实应用后,浏览器加载示例文档、完成一次证据化问答,并校验右侧检索证据与回答引用一致。''', + ), + ] + + for path, anchor, block in updates: + text = path.read_text(encoding='utf-8') + if '' in text: + continue + if anchor not in text: + raise SystemExit('README anchor not found in %s' % path) + path.write_text(text.replace(anchor, block + '\n\n' + anchor, 1), encoding='utf-8') + PY + + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add docs/assets/playground.png README.md README_ch.md + if git diff --cached --quiet; then + echo 'Accepted screenshot and README showcase are already current.' + exit 0 + fi + git commit -m 'docs: publish accepted playground screenshot [skip ci]' + git push origin HEAD:"${HEAD_REF}" + + - name: Upload browser evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: playground-browser-evidence + if-no-files-found: ignore + retention-days: 30 + path: | + docs/assets/playground.png + frontend/playwright-report + frontend/test-results + + - name: Show container logs after failure + if: failure() + run: docker compose logs --no-color + + - name: Stop playground stack + if: always() + run: docker compose down --volumes --remove-orphans diff --git a/README.md b/README.md index f5685ef..c4013c8 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,13 @@ RAG_PLAYGROUND_MODE=ollama docker compose --profile ollama up --build See [the playground guide](docs/PLAYGROUND.md) for supported files, environment variables, API contracts and local development. + +## Visual Playground + +![Java-RAG visual playground with knowledge base, grounded chat, and retrieval evidence](docs/assets/playground.png) + +This screenshot is generated by the Playwright acceptance test after Docker Compose starts the real application, the browser loads the sample document, asks a grounded question, and verifies the matching retrieval evidence. + ## Why Java-RAG? - **Visual and inspectable** — the React playground displays knowledge-base state, conversation and retrieval evidence side by side. diff --git a/README_ch.md b/README_ch.md index 9585e62..5a90860 100644 --- a/README_ch.md +++ b/README_ch.md @@ -62,6 +62,13 @@ RAG_PLAYGROUND_MODE=ollama docker compose --profile ollama up --build 详细说明见 [Playground 使用与 API 文档](docs/PLAYGROUND.md)。 + +## 可视化 Playground + +![Java-RAG 可视化知识库、证据化问答与检索证据界面](docs/assets/playground.png) + +这张截图由 Playwright 验收测试自动生成:Docker Compose 启动真实应用后,浏览器加载示例文档、完成一次证据化问答,并校验右侧检索证据与回答引用一致。 + ## 为什么值得关注? - **有真正的前端产品界面**:知识库、问答和证据并排展示,而不是只有后端类和接口; diff --git a/docs/assets/playground.png b/docs/assets/playground.png new file mode 100644 index 0000000..8960eea Binary files /dev/null and b/docs/assets/playground.png differ diff --git a/frontend/e2e/playground.spec.ts b/frontend/e2e/playground.spec.ts new file mode 100644 index 0000000..1d86ce8 --- /dev/null +++ b/frontend/e2e/playground.spec.ts @@ -0,0 +1,39 @@ +import { expect, test } from '@playwright/test' +import { mkdirSync } from 'node:fs' +import { resolve } from 'node:path' + +test('uploads sample, asks a grounded question, and captures real evidence', async ({ page }) => { + await page.goto('/') + + await expect(page.getByRole('heading', { name: 'Java-RAG Playground' })).toBeVisible() + await expect(page.getByText('local · offline-extractive')).toBeVisible() + + await page.getByRole('button', { name: /加载内置示例/ }).click() + + const documentCard = page.locator('.document-card').filter({ hasText: 'java-rag-playground-guide.md' }) + await expect(documentCard).toBeVisible() + await expect(documentCard).toContainText('chunks') + + const question = page.getByPlaceholder('向已上传文档提问…') + await expect(question).toBeEnabled() + await question.fill('如何启动可视化 Playground?') + await page.getByRole('button', { name: /^发送/ }).click() + + const latestAssistantMessage = page.locator('.message-row.assistant').last() + await expect(latestAssistantMessage).toContainText('docker compose up --build') + await expect(latestAssistantMessage).toContainText('[1]') + + const evidenceCard = page.locator('.evidence-card') + await expect(evidenceCard).toBeVisible() + await expect(evidenceCard).toContainText('java-rag-playground-guide.md') + await expect(evidenceCard).toContainText('Chunk #') + await expect(evidenceCard).toContainText('相似度') + await expect(evidenceCard).toContainText('docker compose up --build') + + const screenshotDirectory = resolve(process.cwd(), '../docs/assets') + mkdirSync(screenshotDirectory, { recursive: true }) + await page.screenshot({ + path: resolve(screenshotDirectory, 'playground.png'), + fullPage: true, + }) +}) diff --git a/frontend/package.json b/frontend/package.json index 2cde229..5c92ce5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,13 +6,15 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "preview": "vite preview" + "preview": "vite preview", + "e2e": "playwright test" }, "dependencies": { "react": "^19.2.8", "react-dom": "^19.2.8" }, "devDependencies": { + "@playwright/test": "1.61.1", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.4", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..f41aa98 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + timeout: 90_000, + expect: { + timeout: 15_000, + }, + fullyParallel: false, + forbidOnly: Boolean(process.env.CI), + retries: process.env.CI ? 1 : 0, + workers: 1, + reporter: process.env.CI + ? [['line'], ['html', { outputFolder: 'playwright-report', open: 'never' }]] + : 'list', + outputDir: 'test-results', + use: { + baseURL: process.env.PLAYGROUND_BASE_URL || 'http://127.0.0.1:3000', + trace: 'retain-on-failure', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + viewport: { width: 1440, height: 960 }, + }, + }, + ], +})