diff --git a/RubricScoring-2.jsx.txt b/RubricScoring-2.jsx.txt
new file mode 100644
index 0000000..0abc799
--- /dev/null
+++ b/RubricScoring-2.jsx.txt
@@ -0,0 +1,315 @@
+import { useMemo, useState } from "react";
+import {
+ ChevronDown,
+ CircleAlert,
+ Gauge,
+ Minus,
+ Plus,
+ RotateCcw,
+} from "lucide-react";
+
+/**
+ * RubricScoring
+ * ---------------------------------------------------------------
+ * Weighted rubric scoring panel for evaluating a submission against
+ * a set of criteria. Each criterion carries a weight (%) and is
+ * scored 1–5; the total is the weighted average, shown as a
+ * percentage and a letter-style band.
+ *
+ * Design notes:
+ * - High-density technical layout (compact rows, thin borders,
+ * monospace numerics) suited to an evaluation/dashboard context
+ * rather than a marketing surface.
+ * - No emoji anywhere — status is conveyed with lucide-react icons.
+ * - Full interactive-state coverage: hover, focus-visible, disabled,
+ * and an empty state when no score has been given yet.
+ */
+
+const DEFAULT_CRITERIA = [
+ {
+ id: "req",
+ label: "ความครบถ้วนของ Requirement",
+ description: "ครอบคลุมทุกเงื่อนไขที่ระบุใน spec หรือ user story",
+ weight: 30,
+ },
+ {
+ id: "code",
+ label: "คุณภาพโค้ด",
+ description: "โครงสร้างชัดเจน อ่านง่าย มี error handling ที่เหมาะสม",
+ weight: 25,
+ },
+ {
+ id: "test",
+ label: "การทดสอบ",
+ description: "มี test ครอบคลุม edge case และรันผ่านจริง",
+ weight: 20,
+ },
+ {
+ id: "doc",
+ label: "เอกสารประกอบ",
+ description: "README / comment เพียงพอให้คนอื่นเข้าใจและต่อยอดได้",
+ weight: 15,
+ },
+ {
+ id: "perf",
+ label: "ประสิทธิภาพ",
+ description: "ไม่มี bottleneck ที่ชัดเจน ใช้ resource อย่างสมเหตุสมผล",
+ weight: 10,
+ },
+];
+
+const SCORE_LABELS = {
+ 1: "ต้องแก้ไขมาก",
+ 2: "ต้องปรับปรุง",
+ 3: "ผ่านเกณฑ์",
+ 4: "ดี",
+ 5: "ดีเยี่ยม",
+};
+
+function bandForPercent(pct) {
+ if (pct >= 90) return { label: "ดีเยี่ยม", tone: "band-excellent" };
+ if (pct >= 75) return { label: "ดี", tone: "band-good" };
+ if (pct >= 60) return { label: "ผ่านเกณฑ์", tone: "band-pass" };
+ return { label: "ต้องปรับปรุง", tone: "band-low" };
+}
+
+function ScoreStepper({ value, onChange, disabled }) {
+ const clamp = (n) => Math.min(5, Math.max(1, n));
+
+ return (
+
+
+
+
+
+ {value ?? "—"}
+ /5
+
+
+ {value ? SCORE_LABELS[value] : "ยังไม่ให้คะแนน"}
+
+
+
+
+
+ );
+}
+
+export default function RubricScoring({
+ title = "แบบประเมิน Rubric",
+ criteria = DEFAULT_CRITERIA,
+ onSubmit,
+}) {
+ const [scores, setScores] = useState({});
+ const [notes, setNotes] = useState({});
+ const [openNoteId, setOpenNoteId] = useState(null);
+
+ const totalWeight = useMemo(
+ () => criteria.reduce((sum, c) => sum + c.weight, 0),
+ [criteria]
+ );
+
+ const scoredCount = Object.keys(scores).length;
+ const isComplete = scoredCount === criteria.length;
+
+ const weightedPercent = useMemo(() => {
+ if (scoredCount === 0) return null;
+ const earned = criteria.reduce((sum, c) => {
+ const s = scores[c.id];
+ if (s == null) return sum;
+ return sum + (s / 5) * c.weight;
+ }, 0);
+ const weightScored = criteria.reduce(
+ (sum, c) => (scores[c.id] != null ? sum + c.weight : sum),
+ 0
+ );
+ if (weightScored === 0) return null;
+ // Show progress against total possible weight, not just what's scored,
+ // so the number reflects the whole rubric rather than a partial subset.
+ return Math.round((earned / totalWeight) * 100);
+ }, [scores, criteria, totalWeight, scoredCount]);
+
+ const band = weightedPercent != null ? bandForPercent(weightedPercent) : null;
+
+ const handleScore = (id, value) => {
+ setScores((prev) => ({ ...prev, [id]: value }));
+ };
+
+ const handleReset = () => {
+ setScores({});
+ setNotes({});
+ setOpenNoteId(null);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+ {title}
+
+
+ {criteria.length} เกณฑ์ · น้ำหนักรวม {totalWeight}%
+
+
+
+
+
+
+ {/* Criteria rows */}
+
+ {criteria.map((c) => {
+ const value = scores[c.id] ?? null;
+ const noteOpen = openNoteId === c.id;
+
+ return (
+ -
+
+
+
+
+ {c.weight}%
+
+
+ {c.label}
+
+
+
+ {c.description}
+
+
+
+
+ {noteOpen && (
+
+
+
handleScore(c.id, v)}
+ />
+
+
+ );
+ })}
+
+
+ {/* Summary */}
+
+
+
+
+
+ ให้คะแนนแล้ว {scoredCount}/{criteria.length} เกณฑ์
+
+
+
+
+ {band && (
+
+ {band.label}
+
+ )}
+
+ {weightedPercent != null ? `${weightedPercent}%` : "—"}
+
+
+
+
+ {/* Progress bar */}
+
+
+ {!isComplete && (
+
+
+ ยังเหลือ {criteria.length - scoredCount} เกณฑ์ที่ยังไม่ได้ให้คะแนน
+
+ )}
+
+
+
+
+ );
+}
diff --git a/example_fastapi_endpoint.py b/example_fastapi_endpoint.py
new file mode 100644
index 0000000..e6a038a
--- /dev/null
+++ b/example_fastapi_endpoint.py
@@ -0,0 +1,116 @@
+"""
+Example endpoint following the fastapi-research skill conventions.
+Use case: submit a research document, kick off background embedding,
+and expose paginated results — matches the doc_embeddings / pgvector
+pipeline noted for CrystalCastle.
+"""
+
+from datetime import datetime
+from typing import Annotated
+from uuid import UUID, uuid4
+
+from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query
+from pydantic import BaseModel, ConfigDict, Field
+
+router = APIRouter(prefix="/research-docs", tags=["research-docs"])
+
+
+# --- 1. Separate Create / Response schemas (never reuse one model) -------
+
+class ResearchDocCreate(BaseModel):
+ title: str = Field(..., min_length=1, max_length=200)
+ content: str = Field(..., min_length=1, description="Raw markdown/text to embed")
+ source_url: str | None = Field(default=None, description="Origin of the document, if any")
+
+
+class ResearchDocResponse(BaseModel):
+ model_config = ConfigDict(from_attributes=True)
+
+ id: UUID
+ title: str
+ status: str # "queued" | "embedded" | "failed"
+ created_at: datetime
+
+
+# --- 2. Dependency injection for shared resources ------------------------
+
+async def get_db_session():
+ # Replace with your actual Supabase/asyncpg session factory.
+ session = await create_session()
+ try:
+ yield session
+ finally:
+ await session.close()
+
+
+DbSession = Annotated[object, Depends(get_db_session)]
+
+
+# --- 3. Background task for the actual embedding work --------------------
+
+async def embed_document(doc_id: UUID, content: str) -> None:
+ """
+ Runs after the response is sent. Generates the embedding
+ (e.g. sentence-transformers all-MiniLM-L6-v2) and upserts it
+ into the pgvector `doc_embeddings` table. Anything over a couple
+ seconds belongs here, not in the request/response cycle.
+ """
+ try:
+ vector = await generate_embedding(content)
+ await upsert_embedding(doc_id=doc_id, vector=vector)
+ except Exception as exc:
+ await mark_doc_failed(doc_id, reason=str(exc))
+
+
+# --- 4. Route handlers ----------------------------------------------------
+
+@router.post("", response_model=ResearchDocResponse, status_code=202)
+async def create_research_doc(
+ payload: ResearchDocCreate,
+ background_tasks: BackgroundTasks,
+ db: DbSession,
+) -> ResearchDocResponse:
+ doc_id = uuid4()
+ now = datetime.utcnow()
+
+ await db.execute(
+ "insert into research_docs (id, title, content, source_url, status, created_at) "
+ "values ($1, $2, $3, $4, 'queued', $5)",
+ doc_id, payload.title, payload.content, payload.source_url, now,
+ )
+
+ background_tasks.add_task(embed_document, doc_id, payload.content)
+
+ return ResearchDocResponse(id=doc_id, title=payload.title, status="queued", created_at=now)
+
+
+@router.get("", response_model=list[ResearchDocResponse])
+async def list_research_docs(
+ db: DbSession,
+ limit: int = Query(default=20, ge=1, le=100),
+ offset: int = Query(default=0, ge=0),
+) -> list[ResearchDocResponse]:
+ rows = await db.fetch(
+ "select id, title, status, created_at from research_docs "
+ "order by created_at desc limit $1 offset $2",
+ limit, offset,
+ )
+ return [ResearchDocResponse.model_validate(dict(r)) for r in rows]
+
+
+@router.get("/{doc_id}", response_model=ResearchDocResponse)
+async def get_research_doc(doc_id: UUID, db: DbSession) -> ResearchDocResponse:
+ row = await db.fetchrow(
+ "select id, title, status, created_at from research_docs where id = $1",
+ doc_id,
+ )
+ if row is None:
+ raise HTTPException(status_code=404, detail="research document not found")
+ return ResearchDocResponse.model_validate(dict(row))
+
+
+# --- Placeholders for the imports referenced above (wire up for real) ----
+async def create_session(): ...
+async def generate_embedding(text: str) -> list[float]: ...
+async def upsert_embedding(doc_id: UUID, vector: list[float]) -> None: ...
+async def mark_doc_failed(doc_id: UUID, reason: str) -> None: ...
diff --git a/example_langchain_rag.py b/example_langchain_rag.py
new file mode 100644
index 0000000..efd23fb
--- /dev/null
+++ b/example_langchain_rag.py
@@ -0,0 +1,96 @@
+"""
+Example RAG chain following the langchain-research skill conventions.
+Use case: answer a question against the same pgvector doc_embeddings
+store the FastAPI endpoint above populates.
+"""
+
+from pydantic import BaseModel, Field
+from tenacity import retry, stop_after_attempt, wait_exponential
+
+from langchain_core.output_parsers import PydanticOutputParser
+from langchain_core.prompts import ChatPromptTemplate
+from langchain_core.runnables import RunnablePassthrough
+from langchain_openai import ChatOpenAI
+from langchain_community.vectorstores import SupabaseVectorStore
+from langchain_openai import OpenAIEmbeddings
+
+
+# --- 1. Structured output instead of freeform text ------------------------
+
+class ResearchAnswer(BaseModel):
+ answer: str = Field(description="Direct answer to the question")
+ sources: list[str] = Field(description="Titles or URLs of documents cited")
+ confidence: float = Field(ge=0, le=1, description="Model's self-rated confidence")
+
+
+parser = PydanticOutputParser(pydantic_object=ResearchAnswer)
+
+
+# --- 2. Prompt kept out of the orchestration logic -------------------------
+# In a real project this lives in prompts/research_qa.yaml, not inline.
+
+SYSTEM_PROMPT = """\
+You are a research assistant answering questions strictly from the provided
+context. If the context does not contain the answer, say so explicitly —
+never fabricate a citation.
+
+{format_instructions}
+"""
+
+prompt = ChatPromptTemplate.from_messages(
+ [
+ ("system", SYSTEM_PROMPT),
+ ("human", "Context:\n{context}\n\nQuestion: {question}"),
+ ]
+).partial(format_instructions=parser.get_format_instructions())
+
+
+# --- 3. Retriever over the shared pgvector store ---------------------------
+
+def build_retriever(supabase_client, table_name: str = "doc_embeddings", k: int = 5):
+ store = SupabaseVectorStore(
+ client=supabase_client,
+ embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
+ table_name=table_name,
+ query_name="match_doc_embeddings", # matching SQL function in Supabase
+ )
+ return store.as_retriever(search_kwargs={"k": k})
+
+
+def format_docs(docs) -> str:
+ return "\n\n".join(f"[{d.metadata.get('title', 'untitled')}] {d.page_content}" for d in docs)
+
+
+# --- 4. Retry wrapper for the flaky-provider case --------------------------
+
+@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
+async def _invoke_llm(chain, inputs: dict) -> ResearchAnswer:
+ return await chain.ainvoke(inputs)
+
+
+# --- 5. Assemble the LCEL chain --------------------------------------------
+
+def build_research_qa_chain(supabase_client, model: str = "gpt-4o-mini"):
+ retriever = build_retriever(supabase_client)
+ llm = ChatOpenAI(model=model, temperature=0)
+
+ chain = (
+ {
+ "context": retriever | format_docs,
+ "question": RunnablePassthrough(),
+ }
+ | prompt
+ | llm
+ | parser
+ )
+ return chain
+
+
+async def answer_research_question(supabase_client, question: str) -> ResearchAnswer:
+ chain = build_research_qa_chain(supabase_client)
+ try:
+ return await _invoke_llm(chain, question)
+ except Exception as exc:
+ # Surface a typed failure instead of letting a raw provider error
+ # propagate — the FastAPI layer maps this to a clean 502/503.
+ raise RuntimeError(f"research QA chain failed after retries: {exc}") from exc
diff --git a/fastapi-research-SKILL.md b/fastapi-research-SKILL.md
new file mode 100644
index 0000000..0bca2b0
--- /dev/null
+++ b/fastapi-research-SKILL.md
@@ -0,0 +1,63 @@
+---
+name: fastapi-research
+description: This skill should be used when the user asks to "build an API endpoint", "design a FastAPI route", "add a Pydantic schema", "implement async data processing", or write FastAPI middleware, dependencies, or research-service integrations.
+when_to_use: Apply when creating or modifying FastAPI endpoints, request/response schemas, background tasks, or API-level error handling in research or data-service codebases.
+argument-hint: [endpoint-or-component-name]
+disable-model-invocation: false
+user-invocable: true
+allowed-tools: Read Edit Write Glob Grep Bash(pytest *) WebFetch
+disallowed-tools: AskUserQuestion
+paths:
+ - "api/**/*.py"
+ - "services/**/*.py"
+ - "research/**/*.py"
+ - "models/**/*.py"
+ - "schemas/**/*.py"
+effort: high
+---
+
+# FastAPI Research API Development
+
+Expert instructions for building scalable, high-performance research APIs using FastAPI.
+
+## Core principles
+
+- **Type safety**: use Pydantic models for rigorous request/response validation.
+- **Async first**: use `async def` for I/O-bound work to maximize throughput.
+- **Dependency injection**: use FastAPI's `Depends` for modularity and testability (DB sessions, auth, config).
+- **Self-documenting**: give every route a clear docstring and type hints so Swagger/OpenAPI stays accurate.
+
+## Implementation standards
+
+### 1. Endpoint design
+- Follow REST conventions: `GET` for retrieval, `POST` for creation, `PATCH` for partial updates, `DELETE` for removal.
+- Group related routes with `APIRouter`, using consistent tags and prefixes.
+- Paginate every list endpoint (`limit`/`offset` or cursor-based).
+
+### 2. Pydantic schemas
+- Define separate `Create`, `Update`, and `Response` models per resource — never reuse one model across all three.
+- Use `Field(...)` to document constraints and units for research parameters.
+- Set `model_config = ConfigDict(from_attributes=True)` for ORM integration.
+
+### 3. Error handling & security
+- Raise `HTTPException` with precise status codes (404 missing resource, 409 conflict, 422 validation, 500 unhandled).
+- Register a global exception handler so error responses share one JSON shape across the API.
+- Protect research endpoints with API keys or OAuth2 scopes as appropriate; never leave internal-only routes unauthenticated.
+
+### 4. Integration & storage
+- Manage DB connections through lifespan events or dependency injection — not module-level globals.
+- Use `BackgroundTasks` for long-running simulations or exports; move anything over a few seconds off the request/response cycle.
+
+## Code style & testing
+
+- Follow PEP 8; type-hint every function signature.
+- Write tests with `TestClient` (or `httpx.AsyncClient`) and `pytest`.
+- Mock external research services and databases in the test suite — no live network calls in unit tests.
+
+## Workflow
+
+1. Define the Pydantic schema for the request/response.
+2. Implement the `async` route handler that accepts it.
+3. Delegate business logic to a service/repository layer, not the route function itself.
+4. Return a validated Pydantic response model.
+5. Run `pytest` to confirm nothing regressed before finishing.
diff --git a/langchain-research-SKILL.md b/langchain-research-SKILL.md
new file mode 100644
index 0000000..ebdd1c6
--- /dev/null
+++ b/langchain-research-SKILL.md
@@ -0,0 +1,60 @@
+---
+name: langchain-research
+description: This skill should be used when the user asks to "build a RAG pipeline", "orchestrate an LLM chain", "add a vector store", "write a research agent", or design prompt templates and multi-step reasoning workflows with LangChain.
+when_to_use: Apply when creating or modifying document-retrieval systems, prompt templates, vector-store integrations, or agentic research workflows built on LangChain.
+argument-hint: [chain-or-research-task]
+disable-model-invocation: false
+user-invocable: true
+allowed-tools: Read Edit Write Glob Grep Bash(pytest *) WebFetch
+disallowed-tools: AskUserQuestion
+paths:
+ - "chains/**/*.py"
+ - "research/**/*.py"
+ - "prompts/**/*.txt"
+ - "prompts/**/*.yaml"
+ - "vectorstores/**/*.py"
+effort: high
+---
+
+# LangChain Research Orchestration
+
+Expert instructions for building robust LLM-powered research tools using LangChain.
+
+## Core principles
+
+- **Modularity**: compose chains from discrete components using LCEL (LangChain Expression Language).
+- **Traceability**: enable verbose logging or LangSmith tracing so reasoning steps can be audited.
+- **Prompt decoupling**: keep prompt templates in versioned files, separate from orchestration logic.
+- **State management**: use the memory component that matches the conversation shape — don't default to full-history buffers for long research sessions.
+
+## Pipeline architecture
+
+### 1. RAG (retrieval-augmented generation)
+- **Ingestion**: use recursive character splitting sized for research-paper structure (respect section boundaries where possible).
+- **Retrieval**: query the vector store (Chroma, Pinecone, etc.) with semantic search; add re-ranking when precision matters more than recall.
+- **Context assembly**: format retrieved chunks clearly (source, section) so the LLM can cite them.
+
+### 2. Chain & agent construction
+- Use `SequentialChain` (or LCEL pipes) for multi-step literature reviews: extract → summarize → synthesize.
+- Give agents scoped research tools (search, Arxiv/PubMed lookups) rather than unrestricted web access.
+- Use `OutputParsers` (or structured output via Pydantic) so research findings return in a predictable JSON shape.
+
+### 3. Prompt engineering
+- Use few-shot examples for complex extraction tasks where format matters.
+- Write an explicit system message defining the assistant's role and constraints — don't rely on the default persona.
+- Inject research context through input variables, not string concatenation.
+
+### 4. Performance & reliability
+- Add retry logic (exponential backoff) for flaky LLM providers.
+- Use `astream` / `astream_events` for real-time feedback in research UIs.
+- Summarize long-running research history before it re-enters the prompt, to control token cost.
+
+## Example usage
+
+- **Document QA**: load a PDF, chunk and embed it, answer technical questions with citations.
+- **Synthesis agent**: search multiple sources and compile a structured summary report.
+
+## Evaluation
+
+- Build an evaluation chain that grades outputs against ground-truth answers.
+- Watch for hallucinated citations or claims not present in retrieved context — flag them rather than silently passing.