diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2b0b405..bb8e3ed 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,6 +10,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+permissions:
+ contents: read
+
jobs:
backend-test:
name: Backend Tests
diff --git a/backend/server/schemas.py b/backend/server/schemas.py
index e0014a3..f48eaa0 100644
--- a/backend/server/schemas.py
+++ b/backend/server/schemas.py
@@ -28,7 +28,8 @@ class ChoiceQuestion(BaseQuestion):
@classmethod
def check_choice(cls, value: dict[str, object] | list[str]) -> dict[str, object] | list[str]:
labels = list(value)
- if any(not label.strip() for label in labels) or len(set(labels)) != len(labels):
+ normalized = [label.strip() for label in labels]
+ if any(not label for label in normalized) or len(set(normalized)) != len(normalized):
raise ValueError("choice labels must be nonempty and unique")
return value
@@ -40,7 +41,8 @@ class ScoreQuestion(BaseQuestion):
@field_validator("criteria")
@classmethod
def check_score(cls, value: list[str]) -> list[str]:
- if any(not label.strip() for label in value) or len(set(value)) != len(value):
+ normalized = [label.strip() for label in value]
+ if any(not label for label in normalized) or len(set(normalized)) != len(normalized):
raise ValueError("score criteria labels must be nonempty and unique")
return value
diff --git a/backend/tests/test_flow.py b/backend/tests/test_flow.py
index 5148857..4692abd 100644
--- a/backend/tests/test_flow.py
+++ b/backend/tests/test_flow.py
@@ -229,6 +229,9 @@ def test_question_criteria_validation():
with pytest.raises(ValidationError, match="score criteria labels must be nonempty and unique"):
ScoreQuestion(instructions="Rate", type="score", criteria=["low", " "])
+ with pytest.raises(ValidationError, match="score criteria labels must be nonempty and unique"):
+ ScoreQuestion(instructions="Rate", type="score", criteria=["low", " low "])
+
ChoiceQuestion(instructions="Choose", type="choice", criteria=["option_a", "option_b"])
ChoiceQuestion(instructions="Choose", type="choice", criteria={"option_a": "desc A", "option_b": "desc B"})
@@ -237,4 +240,3 @@ def test_question_criteria_validation():
with pytest.raises(ValidationError, match="choice labels must be nonempty and unique"):
ChoiceQuestion(instructions="Choose", type="choice", criteria=["opt", " "])
-
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 6d3c925..bea07b2 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -3,6 +3,7 @@ import { Activity, BookOpen, ChartNoAxesColumn, CircleHelp, Copy, KeyRound, Lang
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { apiErrorMessage, useI18n, type Locale, type MessageKey, type Translate } from "./i18n"
+import { Playground, sampleRequest } from "./Playground"
type ApiKey = { id: number; name: string; mask: string; created_at: string; revoked_at: string | null; last_used_at: string | null }
type Usage = { totals: { requests: number; input_tokens: number; output_tokens: number }; daily: { day: string; requests: number; input_tokens: number; output_tokens: number }[]; sources: { source: string; key_id: number | null; requests: number; input_tokens: number; output_tokens: number }[] }
@@ -115,7 +116,7 @@ function App() {
{title}LAYA SERVER
-
{page === "home" ? : page === "keys" ? : page === "usage" ? : page === "playground" ? : }
+
{page === "home" ? : page === "keys" ? : page === "usage" ? : page === "playground" ? api("/internal/playground", { method: "POST", body }, session.csrf_token)} /> : }
{mobileNav ?