Join the Agent Deck Discord Server!
Discord에서 Agent Deck 커뮤니티 확인, 357명과 자유 음성·텍스트 채팅 이용
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기言葉 Kotoba Engine — a community board for annotating the pragmatic, cultural nuance behind real Japanese text, beyond what dictionaries tell you.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A community board for annotating the pragmatic, high-context meaning behind real
Japanese text — the politeness registers, implicit social cues, and slang that dictionary
apps miss. Users post real Japanese sentences, the app tokenizes them into morphological
components on the server, and the community pins nuance notes to individual word tokens in
real time — plus general comment threads, following, streaks, badges, and a leaderboard on
top of that.README 전문 보기 ↗A community board for annotating the pragmatic, high-context meaning behind real Japanese text — the politeness registers, implicit social cues, and slang that dictionary apps miss. Users post real Japanese sentences, the app tokenizes them into morphological components on the server, and the community pins nuance notes to individual word tokens in real time — plus general comment threads, following, streaks, badges, and a leaderboard on top of that.
Live at: https://kotoba-connect-three.vercel.app
Core board
<ruby> furigana above the kanji, or
transliterates the sentence to romaji via wanakana — applies everywhere Japanese text
renders (board, search, entry detail, the token-annotation view)is_bot flag) seeds starter entries with real AI nuance +
embeddings so the board doesn't read empty for new users; excluded from the leaderboardtoggle_entry_upvote) backed by a unique
(user_id, entry_id) ledger — prevents duplicate votes under concurrencyCommunity & profile
system messages
always come through regardless)@handle app-wide once set) and 40 preset
avatar icons across 4 themed sets — no image upload/storage neededsignInWithPassword re-auth check before updateUser runs) and starts collapsed behind a
prompt rather than sitting open by default; account deletion; EN/JP language toggle
(cookie-backed, hand-translated — not machine translation, see lib/i18n/)app/settings/Identity & polish
app/globals.css + tailwind.config.ts), and Edge, a
separate true-OLED-black theme built for zero visual clutter (flattened surfaces, no paper
texture, formality badges as colored text instead of solid pills) rather than just a darker
dark mode:root { font-size: 117% }) applied to every theme on
every screen size — a real viewport-based baseline, not a desktop-only zoom hack (an earlier
approach that caused a round of "auto-zoomed"-looking pages on narrow phones; root-caused to
a missing min-w-0 letting one oversized row set the width floor for the entire app shell —
see the comment in app/layout.tsx)touchstart listener (components/EnableTapFeedback.tsx) makes every :active
press-state style in the app actually fire on iOS Safari, which otherwise silently suppresses
:active entirely unless some element on the page has a touch listenerloading.tsx + matching skeletons for every main page, so client-side navigation
shows an instant, correctly-shaped placeholder (via Next's automatic Suspense boundary) instead
of a frozen screen while the destination page's data fetcheslib/entry-interaction-store.ts) instead of each copy's own disconnected local staterobots.txt, sitemap.xml, PWA manifest, custom 404, generated faviconbeforeinstallprompt; no-op by design on iOS Safari, which has no programmatic install), and
Web Push notifications (public/sw.js, lib/push.ts, lib/use-push-subscription.ts) — a
per-device opt-in from Settings → Notifications that re-delivers the same activity types
(likes, annotations, comments, follows) even when the tab isn't open, feature-flagged on VAPID
keys being configuredProduction hardening
profiles row per
user and a proper email-confirmation flow (pending screen + resend)/admin
(dismiss or delete-and-resolve), and an is_admin flag on profiles/api/tokenize and the AI endpointsauth.getSession() read
(re-verified for real via auth.getUser() at most once every 5 minutes, tracked in a cookie),
while every /api/* route always pays for the real auth.getUser() round-trip, since that's
the surface where this app's own server-side code makes authorization decisions (account
deletion, admin actions). Trades a bounded ~5-minute window (worst case) before a revoked
session stops looking signed-in on ordinary pages, for cutting a network round-trip off nearly
every request — see lib/supabase/middleware.ts. Either way, the resolved user id is forwarded
to the page render via a request header so Server Components read it locally instead of calling
auth.getUser() again themselvesSENTRY_DSNPhase 2 (feature-flagged — the app runs fine without these keys)
/api/entries/[id]/analyze)/api/entries/[id]/embed); the search bar on the board calls /api/search, which embeds the
query and matches via the match_entries pgvector RPC/api/bot/chat), Groq-backed, and a real retrieval-augmented agent rather
than a plain wrapper around an LLM: every message runs a pgvector semantic search
(match_entries, the same RPC the search bar uses) over the board's own submitted entries, and
relevant results are (a) fed to the model as grounding context it's instructed to cite rather
than invent, and (b) rendered separately in the UI as clickable "Found on the board" citation
chips. It's also entry-context-aware — opened from an entry's detail page, it's additionally
grounded in that specific sentence (EntryChatContext)/api/entries/assist): Groq suggests a few topic tags and, if the
translation field is still empty, a draft translation; separately, the OpenAI embeddings
already generated per entry are reused to warn when a near-duplicate entry already exists/api/entries/ocr): a Groq vision model (Llama 4 Scout) transcribes Japanese
text from an uploaded photo — manga panel, street sign, a screenshot — straight into the
compose box; the image itself is never stored, only the extracted text/api/reports/[id]/triage): Groq pre-screens each filed report for
likely severity so the admin queue surfaces credible violations (spam, harassment, non-Japanese
junk) first — explicitly never treats rude/slang Japanese content itself as a violation,
since this app catalogs that as data on purpose (see Security notes)Install dependencies
npm install
Create a Supabase project at supabase.com, then open the SQL Editor and run, in order:
supabase/schema.sql — base tables, RLS, upvote RPCsupabase/migrations/, in filename order —
moderation/admin, DB rate limits, AI columns, pgvector search, platform features
(bookmarks/notifications), the mascot bot, comments, follows, blocks, streaks, profile
customization (display names/avatars), notification preferences, and push subscriptionsConfigure environment variables
cp .env.example .env.local
At minimum, fill in NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY from
Project Settings → API. Everything else in .env.example is optional — see below.
Make yourself an admin (optional, to see /admin): in the SQL editor,
update public.profiles set is_admin = true where id = '<your-user-uuid>';
Run the dev server
npm run dev
Open http://localhost:3000, create an account, and post a
sentence — try お先に失礼します or マジ卍.
| Feature | Env var(s) | Where to get it |
|---|---|---|
| Google sign-in | (none — configured in Supabase) | Supabase Dashboard → Authentication → Providers → enable Google, add your OAuth client ID/secret, and set the redirect URL to https://<your-domain>/auth/callback |
| Rate limiting | UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN | upstash.com (free tier) |
| Error monitoring | SENTRY_DSN (+ SENTRY_ORG/SENTRY_PROJECT/SENTRY_AUTH_TOKEN for source maps) | sentry.io |
| AI nuance classification, mascot chat bot, compose-time assist, OCR-from-photo, moderation triage | GROQ_API_KEY | console.groq.com (free tier) |
| Semantic search, duplicate-entry nudge | GEMINI_API_KEY | aistudio.google.com (free tier — no card required) |
| Correct Open Graph image URLs in production | NEXT_PUBLIC_SITE_URL | your deployed domain, e.g. https://kotoba-connect-three.vercel.app |
| Account deletion, AI moderation triage persistence | SUPABASE_SERVICE_ROLE_KEY | Supabase Project Settings → API → service_role |
| Web Push notifications | NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT (a mailto: address), SUPABASE_WEBHOOK_SECRET | Generate a VAPID pair with npx web-push generate-vapid-keys; also requires running supabase/migrations/0016_push_subscriptions.sql. To actually fire a push when a notifications row is inserted (not just the self-test button in Settings): pick any random string for SUPABASE_WEBHOOK_SECRET, then in the Supabase Dashboard go to Database → Webhooks → Create a new hook — table , event , type , method , URL , and add an HTTP header set to that same string |
Each is independently optional — the app degrades gracefully (features just don't activate) without them.
.env.example you're using, in the Vercel project
settings.node_modules/kuromoji/dict and is bundled
automatically with the Node.js serverless function for /api/tokenize..github/workflows/ci.yml) runs on every push/PR to main: TypeScript
type-checking (tsc --noEmit), ESLint (next/core-web-vitals), unit tests (Vitest), and a
full production build.npm test, lib/__tests__/) cover the pure logic most likely to break silently:
EN/JA translation parity (every key present in both languages, with matching {placeholders}),
the Gemini embeddings client (L2 normalization, task types, 1536-dim truncation, API key sent
in a header rather than the URL, graceful null on failure — all against a mocked fetch),
spam heuristics (links/floods blocked, rude-register Japanese allowed), katakana→hiragana
furigana conversion, avatar preset tokens, and rate-limit client-IP extraction.next.config.js): X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, and Permissions-Policy on every response, X-Powered-By removed. CSP is
deliberately not set — see the comment there for why.prefers-reduced-motion setting.app/error.tsx recovers a single crashed page without losing
the surrounding nav (reported to Sentry, with a retry button), while app/global-error.tsx is
the last-resort fallback if the root layout itself throws.lib/moderation.ts (link/flood detection) are client-side UX, not a security boundary; real
defense is RLS + the DB-level rate-limit triggers in
supabase/migrations/0002_db_rate_limits.sql + the report/admin-review flow.npm audit currently flags a Next.js advisory (Image Optimization AVIF RCE) with no patch
backported to the 14.x line as of this writing — the fix requires Next 16. This app doesn't
use next/image with AVIF, but if that changes, re-evaluate before upgrading.A running log of the non-obvious engineering calls in this project, in the order they were made — what was chosen, why, and what was rejected instead. Kept honest: including the one listed as "decided against" below, and the embeddings-provider switch that reversed an earlier choice once its real tradeoffs showed up in production.
| # | Decision | Why | Rejected instead |
|---|---|---|---|
| 1 | Next.js 14 App Router + Supabase (Postgres, Auth, Realtime, RLS) as the entire backend | One platform covers relational data, auth, live subscriptions, and row-level authorization — no separate API server to design, deploy, or keep in sync | A custom Node/Express API layer in front of Postgres — more moving parts for no capability this app actually needed |
| 2 | Groq for all LLM inference (pragmatic-read generation, OCR-from-photo, the mascot chat bot, moderation triage) | Fast inference on a genuinely usable free tier, for a project with no revenue to fund a paid API from day one | OpenAI/Anthropic chat APIs — better output quality on some tasks, but no free tier that survives real traffic |
| 3 | pgvector for semantic search, inside the same Postgres database | No second system to run, back up, or keep consistent with the primary data | A dedicated vector database (Pinecone, Weaviate, etc.) — unjustified operational overhead at this scale |
| 4 | RLS is the actual security boundary, not the Next.js API routes | Entry/vote/follow/block/report writes go straight from the browser to Supabase; a route that looked like the enforcement point would be a false sense of security if RLS itself weren't sound | Funneling all writes through API routes to "check permissions in application code" — redundant with RLS and easy to let drift out of sync with it |
| 5 | No profanity/slang filtering | Annotating rude, informal, and slang registers is the product itself — a word-list filter would produce false positives against the app's own purpose. Abuse is handled via user reports + admin review instead | Automated word-list blocking, the default choice for most community platforms |
| 6 | Blocking enforced client-side (filtered out of fetched rows), not via RLS | A blocked user's posts stay publicly visible to everyone else; blocking only changes what the blocker personally sees, which is the actual product intent | RLS-level blocking — would make a blocked user's content invisible platform-wide, which is a ban, not a personal block |
| 7 | Middleware auth check uses getUser(), not getSession() | getUser() re-verifies against the Supabase Auth server on every request; getSession() trusts a locally-decoded JWT that can still look "valid" for a session that's actually been revoked | — faster (no network round-trip) but capable of showing a revoked session as signed-in for up to its token lifetime |
app/
page.tsx Board (server component, fetches session)
login/, forgot-password/, reset-password/ Auth flow pages
entries/[id]/page.tsx Token-annotation + comments detail view
entries/[id]/opengraph-image.tsx Per-entry dynamic OG share image
opengraph-image.tsx Site-wide dynamic OG share image
u/[username]/page.tsx Public profile (follow/block, streak, badges)
tags/[tag]/page.tsx Browse entries by tag
bookmarks/, leaderboard/ Saved entries, reputation ranking
settings/ Grouped drill-down list + profile/account/privacy/
notifications/appearance/reading sub-pages, each with its
own loading.tsx
admin/page.tsx Moderation queue (admin-only)
terms/, privacy/ Legal pages
not-found.tsx, robots.ts, sitemap.ts, manifest.ts, icon.tsx SEO/PWA
about/page.tsx Credibility page — why it exists, AI's role, who built it
auth/actions.ts Server actions for sign in/up/out/resend/reset
auth/callback/route.ts OAuth + email-confirmation redirect handler
api/push/subscribe/, unsubscribe/, send-test/, notify/ Web Push subscription CRUD,
self-test send, and the Database Webhook receiver that
turns a new notifications row into a real push
api/tokenize/route.ts Kuromoji tokenization endpoint (rate-limited)
api/entries/[id]/analyze/route.ts Groq pragmatic classification (Phase 2)
api/entries/[id]/embed/route.ts Gemini embedding for hybrid search (Phase 2)
api/admin/backfill-embeddings/route.ts Admin-triggered batch catch-up for entries
whose embedding is still null (see Architecture decisions)
api/entries/assist/route.ts Compose-time AI assist: tags, translation draft, duplicates
api/entries/ocr/route.ts OCR-from-photo transcription (Groq vision)
api/reports/[id]/triage/route.ts AI moderation-severity triage
api/search/route.ts Semantic search endpoint (Phase 2)
api/bot/chat/route.ts Mascot chat bot (Phase 2)
api/admin/bot/setup/, seed/ Create/seed the official bot account
components/
EntryBoard.tsx / BoardControls.tsx Client board: fetch, realtime, sort/filter, search
EntryForm.tsx Create entry — tokenizer preview, AI assist, photo OCR
EntryCard.tsx / EntryDetail.tsx / EntryComments.tsx List item / annotation canvas / comments
JapaneseText.tsx / TokenizedText.tsx Reading-aid-aware Japanese text renderers (card preview
/ interactive click-to-annotate — see lib/reading-aid-store.ts)
FollowButton.tsx / BlockButton.tsx / BlockedUsersManager.tsx Social graph
ProfileBadges.tsx / Avatar.tsx / AvatarPicker.tsx Profile customization
ReportButton.tsx / AdminQueue.tsx Moderation UI (AI-triaged severity sort)
NotificationBell.tsx Notification inbox
Mascot.tsx / mascots/candidates.tsx / MascotChat.tsx Mascots + entry-aware chat bot widget
EntryChatContext.tsx Hands the bot "the entry currently being viewed"
SideRail.tsx / MobileNav.tsx / auth/ClientAuthProvider.tsx App shell + shared client auth state
EnableTapFeedback.tsx Makes :active press states fire reliably on iOS Safari
InstallPrompt.tsx Dismissible "Add to home screen" banner (beforeinstallprompt)
AiNuanceCallout.tsx Displays Groq's pragmatic read on an entry (+ a trust-hint
line framing it as a starting point, not a settled fact)
BotSeedPanel.tsx Admin UI to create/seed the official bot account
settings/ SettingsGroup/Row/SubpageHeader (drill-down list chrome) +
one form component per settings category
skeletons/ Route-level loading.tsx building blocks
lib/
supabase/ Browser / server / middleware Supabase clients
tokenizer.ts Cached Kuromoji tokenizer builder
groq.ts / embeddings.ts Phase 2 AI integrations (feature-flagged)
rate-limit.ts Optional Upstash-backed limiter
moderation.ts Spam heuristics + report reasons
avatar-presets.ts 40 preset avatar icons (4 sets)
og-font.ts / site.ts OG image font loading + site URL constant
entry-interaction-store.ts / use-entry-interaction.ts Cross-instance like/bookmark sync
reading-aid-store.ts / use-reading-aid.ts / kana.ts Furigana/romaji preference + conversion
bot-entries.ts / bot-auth.ts Starter entry bank + auth guard for the bot admin endpoints
push.ts / use-push-subscription.ts Server-side Web Push send helper + client subscribe hook
use-blocked-ids.ts / cursor-tracker.ts / use-mascot-gaze.ts Small client hooks
i18n/ EN/JP dictionary + server/client locale helpers
public/sw.js Service worker — push + notificationclick handlers only, no
offline caching
supabase/
schema.sql Base schema (fresh install)
migrations/ Incremental SQL — see list below
types/database.ts Shared TypeScript types
| File | Adds |
|---|---|
0001_moderation.sql | report_flags, is_admin |
0002_db_rate_limits.sql | Postgres-trigger rate limits |
0003_ai_pragmatics.sql | Groq nuance/formality columns on entries |
0004_vector_search.sql | pgvector embedding column + match_entries RPC (superseded by hybrid_search in 0020) |
0005_platform_features.sql | bio/website, bookmarks, notifications, tag index |
0006_bot.sql | System-typed bot notifications, welcome message |
0007_comments.sql | General (non-token-pinned) comment threads |
0008_follows.sql | Following users |
0009_blocks.sql | Blocking/muting users |
0010_streaks.sql | Daily posting streaks |
0011_profile_customization.sql | Display names, preset avatar icons |
0012_moderation_triage.sql | ai_severity/ai_reasoning on report_flags |
0013_mandatory_display_name.sql | Backfills/enforces a display name at signup |
0014_bot_account.sql | is_bot flag on profiles (official bot account) |
0015_notification_preferences.sql | notification_prefs jsonb on profiles + updates the four notification-producing trigger functions to check it |
0016_push_subscriptions.sql | push_subscriptions table (one row per subscribed browser/device), RLS scoped to the owning user |
0017_default_avatar.sql | New profiles get a random preset icon instead of a null avatar_url; backfills existing ones |
0018_follows_realtime.sql | Adds user_follows to the supabase_realtime publication — without this, follower counts could only ever update on a page reload, never live |
Author: Sushil Raj (CSE, SRMIST Kattankulathur)
notificationsInsertHTTP RequestPOSThttps://<your-domain>/api/push/notifyx-webhook-secretgetSession()| 8 | Decided against adding SWR/React Query for client-side data caching | The product's actual requirement is that nothing ever renders stale-then-corrects — a follow count, a new comment, a notification should be right immediately. A cache-then-revalidate library's whole value proposition (instant render from a possibly-stale cache) works against that. Realtime Postgres subscriptions + optimistic local state cover the "feels instant" goal without a stale-data window | SWR/React Query — the default reach for "make repeat navigation feel faster" in most React apps, correctly identified as valuable in general, rejected specifically for this app's realtime-correctness requirement |
| 9 | Live updates via Supabase's postgres_changes realtime subscriptions everywhere social state changes (follower counts, notifications, live annotations/votes/comments) | State changes made by other users need to appear without the viewer refreshing — this is a live community board, not a static feed | Polling on an interval — simpler, but either wastes requests polling for nothing or feels laggy waiting for the next tick |
| 10 | Custom weighted, time-decayed engagement score (get_top_contributors, 0019_engagement_score.sql) replacing a raw reputation_score/upvote-count sort for "Top contributors" | A raw upvote count lets someone who posted twice and got lucky outrank someone who annotates, comments, and votes daily. Modeled on Reddit/HN-style recency decay combined with Stack-Overflow-style per-action weighting (posting > annotating > commenting > voting), computed as one SQL function | Sorting by reputation_score directly — the original, simpler approach, replaced once it visibly rewarded the wrong behavior |
| 11 | Semantic search embeddings: started on OpenAI (text-embedding-3-small), migrated to Gemini (gemini-embedding-001, truncated to 1536 dimensions via Matryoshka outputDimensionality so no schema migration was needed) | Checked current terms directly rather than assuming: OpenAI has no standing free tier; Cohere's free key is trial-only and explicitly disallowed in production; Jina's published free-tier numbers disagreed across sources. Gemini's free tier is both ongoing (not a one-time trial credit) and explicitly production-allowed | Staying on OpenAI (real money for a project with no revenue) or Cohere (would've meant paying the moment this stopped being a demo) |
| 12 | Hybrid search — Postgres full-text search (tsvector/ts_rank) fused with vector similarity via Reciprocal Rank Fusion (hybrid_search, 0020_hybrid_search.sql) — replacing a single cosine-similarity threshold | A fixed threshold couldn't be tuned well in Gemini's embedding space: loose enough to admit a real match, it also admitted same-register noise (a query like "I'm cooked" pulling in "I'm hungry", "I'm too sleepy" — same casual sentence shape, wrong meaning). RRF merges rank positions from two independent signals instead of thresholding one score, which sidesteps the tuning problem rather than chasing a better magic number. Followed Supabase's own published pattern for this exact stack; reported to move retrieval precision from ~62% (vector-only) to ~84% (hybrid + RRF) in published benchmarks | A single similarity threshold (tried first at 0.4, then 0.6, then a relative-gap heuristic) — each pass fixed one query's results while breaking another's, which is the actual signal that the fixed-threshold approach was the wrong tool, not that the number was wrong |
| 13 | Text-to-speech via the browser's built-in SpeechSynthesis API | Free, no backend, no API key, ships immediately — and good enough as a first pass for "how is this pronounced" | VOICEVOX — also genuinely free (open-source), but self-hosted: it needs an always-on server, which is real infrastructure this project doesn't otherwise require. Documented as the upgrade path if browser voice quality turns out to be a real complaint, not ruled out |
| 14 | "Card density" (how much of a card's content shows by default vs. behind a tap-to-expand) split out as its own setting, independent of color theme | The collapsed/expandable insight box was originally only reachable by switching to the all-black "Edge" theme — bundling a layout density preference into a color choice meant someone who liked Edge's information density but not its color had no way to get one without the other | Leaving density as an Edge-only behavior — simpler, but forces an unrelated tradeoff on anyone who wants just one of the two |
| 15 | Web Push delivery via a Supabase Database Webhook (fires on notifications insert) calling a Next.js route, rather than a polling job | A push should fire the moment a notification is created, not on the next tick of a cron job | A scheduled job scanning for unsent notifications — adds latency and a job to keep running for no benefit over a webhook that already exists |
| 16 | CI builds against fake NEXT_PUBLIC_SUPABASE_* placeholders (.github/workflows/ci.yml) rather than a real Supabase project | No route is statically prerendered against live data (everything data-backed is dynamically rendered at request time), so type-checking, linting, and a full next build all pass without any real credentials — meaning CI needs zero secrets and can't leak one | Provisioning a dedicated CI-only Supabase project — real infrastructure to maintain for a check that doesn't actually need live data |
| 17 | Page routes run on Vercel's Node.js runtime, after briefly moving all 21 to the Edge Runtime | Edge was adopted to cut cold-start latency, but Vercel bundles a project's Edge pages together, and the bundle crossed the Hobby plan's 1 MB Edge Function limit — failing every production build while the last good deploy kept serving silently. Node.js functions have a 250 MB limit, so page size can't block a deploy again | Staying on Edge and trimming the bundle under 1 MB — would have to be re-fought on every feature that adds client code |
0019_engagement_score.sql | get_top_contributors RPC — weighted, time-decayed engagement score replacing a raw reputation_score sort |
0020_hybrid_search.sql | fts tsvector column + GIN index, and hybrid_search RPC (full-text + vector fused via Reciprocal Rank Fusion) |