Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Client-visible Cloudflare Turnstile site key
NEXT_PUBLIC_TURNSTILE_SITE_KEY=

# Server-only secrets
TURNSTILE_SECRET_KEY=
OPENROUTER_API_KEY=
# Set models explicitly so a provider/model change cannot silently alter cost.
OPENROUTER_CHAT_MODEL=
OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
# Optional OpenRouter attribution headers
OPENROUTER_SITE_URL=
OPENROUTER_APP_NAME=Yoh Kaminaga Portfolio
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

# Optional limits (defaults shown)
ASK_IP_LIMIT=5
ASK_GLOBAL_DAILY_LIMIT=50
ASK_TIMEOUT_MS=12000
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run build
env:
NEXT_TELEMETRY_DISABLED: 1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# local visual QA artifacts
/design-*.png
76 changes: 56 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,72 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# Yoh Kaminaga Portfolio

## Getting Started
神永陽の公開プロジェクトと学習記録を、ターミナルUIで紹介するポートフォリオです。画面右下の「ぎぺん」は、表示中のセクションに反応し、明示的に「質問する」を選んだときだけAI質問画面を開きます。

First, run the development server:
## Tech stack

- Next.js 15 / React 19 / TypeScript
- Motion(スクロール・マスコットアニメーション)
- OpenRouter API(ポートフォリオ内の情報に限定した回答)
- Cloudflare Turnstile(bot対策)
- Upstash Redis(rate limit・embedding cache)
- Vitest / ESLint / GitHub Actions

## Local setup

```bash
npm ci
cp .env.example .env.local
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
AI機能を使わずUIだけ確認する場合、環境変数は空のままで構いません。必要な値が1つでも欠けると、AI機能は安全のため無効になります。

| Environment variable | Purpose |
| --- | --- |
| `NEXT_PUBLIC_TURNSTILE_SITE_KEY` | ブラウザで表示するTurnstile site key |
| `TURNSTILE_SECRET_KEY` | Turnstile server verification |
| `OPENROUTER_API_KEY` | OpenRouterのサーバー専用API key |
| `OPENROUTER_CHAT_MODEL` | 回答生成モデル(明示指定・必須) |
| `OPENROUTER_EMBEDDING_MODEL` | embeddingモデル(例: `openai/text-embedding-3-small`) |
| `OPENROUTER_SITE_URL` | OpenRouterへ送る任意のサイトURL (`HTTP-Referer`) |
| `OPENROUTER_APP_NAME` | OpenRouterへ送る任意のアプリ名 (`X-OpenRouter-Title`) |
| `UPSTASH_REDIS_REST_URL` | Upstash Redis REST endpoint |
| `UPSTASH_REDIS_REST_TOKEN` | Upstash Redis access token |
| `ASK_IP_LIMIT` | 10分あたりのIP別上限(既定: 5) |
| `ASK_GLOBAL_DAILY_LIMIT` | 1日あたりの全体上限(既定: 50) |
| `ASK_TIMEOUT_MS` | OpenRouter timeout(既定: 12000ms) |

Turnstileでは、本番ドメインとは別に `localhost` を許可したsite keyを用意してください。秘密値を `NEXT_PUBLIC_` 付きの変数へ入れないでください。

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
`OPENROUTER_API_KEY` は `.env.local` またはVercelのEnvironment Variablesへ登録し、Gitへcommitしないでください。OpenRouter側では、万一の濫用時にも請求が膨らまないよう、用途専用keyを作成してcredit limitを小さく設定する運用を推奨します。モデル名は価格と提供状況を確認して明示的に設定してください。

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Security design

## Learn More
- API入力は `{ prompt, turnstileToken }` のみを受け取り、質問は200文字までに制限
- Turnstile検証後に、IP別5回/10分・全体50回/日のrate limitを適用
- コーパスembeddingは内容のSHA-256 digestをキーに30日間キャッシュ
- OpenRouterの出力token数、timeout、retry回数、最終文字数を制限
- promptや回答本文をログへ記録しない
- 内部エラー、外部サービスの応答、秘密値をクライアントへ返さない

To learn more about Next.js, take a look at the following resources:
IP制限はVercel等の信頼できるreverse proxyが設定する `x-forwarded-for` の先頭値を利用します。独自インフラへ移す場合は、信頼するproxyとヘッダーの設定を再確認してください。

## Validation

```bash
npm run lint
npm run typecheck
npm test
npm run build
npm audit --omit=dev
```

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
## AI disclosure

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
このポートフォリオの質問機能はOpenRouter経由で設定した生成AIモデルを利用します。回答対象はリポジトリ内の短い紹介文に限定し、記載がない情報を推測しないよう指示しています。ただし生成内容が常に正しい保証はありません。プロジェクトの正確な内容は各GitHubリポジトリを確認してください。

## Deploy on Vercel
実装とリファクタリングではAIコーディング支援を利用し、内容・セキュリティ設計・動作確認は人間が確認する前提です。

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
## Credits

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
「ぎぺん」の画像素材は、ユーザー提供の既存ポートフォリオ素材を再利用しています。無断での再配布・二次利用は行わないでください。
11 changes: 11 additions & 0 deletions design-qa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Design QA

- Reference desktop: `/private/tmp/portfolio-reference-desktop.png`
- Reference mobile: `/private/tmp/portfolio-reference-mobile.png`
- Implementation desktop: `design-implementation-desktop.png`
- Implementation mobile: `design-implementation-mobile.png`
- Compared viewports: 1440 × 900, 390 × 844
- P0 / P1 / P2 differences: none
- Intentional P3 difference: Gipen's speech bubble is slightly taller to include the AI question button.

final result: passed
Loading
Loading