diff --git a/src/app/(app)/problem-editor/[problemId]/page.tsx b/src/app/(app)/problem-editor/[problemId]/page.tsx deleted file mode 100644 index 3e2be4c..0000000 --- a/src/app/(app)/problem-editor/[problemId]/page.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client"; - -import { ProblemFlexLayout } from '@/features/problems/components/problem-flexlayout'; -import EditDescriptionPanel from '@/components/creater/edit-description-panel'; -import EditSolutionPanel from '@/components/creater/edit-solution-panel'; -import EditTestcasePanel from '@/components/creater/edit-testcase-panel'; -import EditDetailPanel from '@/components/creater/edit-detail-panel'; -import EditCodePanel from '@/components/creater/edit-code-panel'; -import { updateProblem } from '@/app/actions/updateProblem'; - -interface ProblemEditorPageProps { - params: Promise<{ problemId: string }>; -} - -interface UpdateData { - content: string; - language?: 'c' | 'cpp'; - inputs?: Array<{ index: number; name: string; value: string }>; -} - -const handleUpdate = async ( - updateFn: (data: UpdateData) => Promise<{ success: boolean }>, - data: UpdateData -) => { - try { - const result = await updateFn(data); - if (!result.success) { - // 这里可以添加更具体的错误处理 - } - return result; - } catch (error) { - console.error('更新失败:', error); - return { success: false }; - } -}; - -export default async function ProblemEditorPage({ - params, -}: ProblemEditorPageProps) { - const { problemId } = await params; - - const components: Record = { - description: { - await handleUpdate( - (descriptionData) => updateProblem({ - problemId, - displayId: 0, - description: descriptionData.content - }), - data - ); - }} - />, - solution: { - await handleUpdate( - (solutionData) => updateProblem({ - problemId, - displayId: 0, - solution: solutionData.content - }), - data - ); - }} - />, - detail: { - await handleUpdate( - (detailData) => updateProblem({ - problemId, - displayId: 0, - detail: detailData.content - }), - data - ); - }} - />, - code: { - await handleUpdate( - (codeData) => updateProblem({ - problemId, - displayId: 0, - templates: [{ - language: codeData.language || 'c', // 添加默认值 - content: codeData.content - }] - }), - data - ); - }} - />, - testcase: { - await handleUpdate( - (testcaseData) => updateProblem({ - problemId, - displayId: 0, - testcases: [{ - expectedOutput: testcaseData.content, - inputs: testcaseData.inputs || [] // 添加默认空数组 - }] - }), - data - ); - }} - /> - }; - - return ( -
- -
- ); -} \ No newline at end of file diff --git a/src/app/(app)/problems/[problemId]/layout.tsx b/src/app/(app)/problems/[problemId]/layout.tsx index def7321..fe22fac 100644 --- a/src/app/(app)/problems/[problemId]/layout.tsx +++ b/src/app/(app)/problems/[problemId]/layout.tsx @@ -1,15 +1,12 @@ import { notFound } from "next/navigation"; import { ProblemHeader } from "@/features/problems/components/header"; -interface ProblemLayoutProps { +interface LayoutProps { children: React.ReactNode; params: Promise<{ problemId: string }>; } -export default async function ProblemLayout({ - children, - params, -}: ProblemLayoutProps) { +const Layout = async ({ children, params }: LayoutProps) => { const { problemId } = await params; if (!problemId) { @@ -24,4 +21,6 @@ export default async function ProblemLayout({ ); -} +}; + +export default Layout; diff --git a/src/app/(app)/problems/[problemId]/page.tsx b/src/app/(app)/problems/[problemId]/page.tsx index a0bb4e9..8ae3f1e 100644 --- a/src/app/(app)/problems/[problemId]/page.tsx +++ b/src/app/(app)/problems/[problemId]/page.tsx @@ -1,39 +1,17 @@ -import { TestcasePanel } from "@/features/problems/testcase/panel"; -import { BotPanel } from "@/features/problems/bot/components/panel"; -import { CodePanel } from "@/features/problems/code/components/panel"; -import { DetailPanel } from "@/features/problems/detail/components/panel"; -import { SolutionPanel } from "@/features/problems/solution/components/panel"; -import { SubmissionPanel } from "@/features/problems/submission/components/panel"; -import { DescriptionPanel } from "@/features/problems/description/components/panel"; -import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout"; +import { ProblemView } from "@/features/problems/ui/views/problem-view"; -interface ProblemPageProps { +interface PageProps { params: Promise<{ problemId: string }>; searchParams: Promise<{ submissionId: string | undefined; }>; } -export default async function ProblemPage({ - params, - searchParams, -}: ProblemPageProps) { +const Page = async ({ params, searchParams }: PageProps) => { const { problemId } = await params; const { submissionId } = await searchParams; - const components: Record = { - description: , - solution: , - submission: , - detail: , - code: , - testcase: , - bot: , - }; + return ; +}; - return ( -
- -
- ); -} +export default Page; diff --git a/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx b/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx new file mode 100644 index 0000000..fb46c63 --- /dev/null +++ b/src/app/(protected)/admin/problems/[problemId]/edit/layout.tsx @@ -0,0 +1,19 @@ +import { notFound } from "next/navigation"; +import { ProblemEditLayout } from "@/features/admin/ui/layouts/problem-edit-layout"; + +interface LayoutProps { + children: React.ReactNode; + params: Promise<{ problemId: string }>; +} + +const Layout = async ({ children, params }: LayoutProps) => { + const { problemId } = await params; + + if (!problemId) { + return notFound(); + } + + return {children}; +}; + +export default Layout; diff --git a/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx b/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx new file mode 100644 index 0000000..8f8302f --- /dev/null +++ b/src/app/(protected)/admin/problems/[problemId]/edit/page.tsx @@ -0,0 +1,13 @@ +import { ProblemEditView } from "@/features/admin/ui/views/problem-edit-view"; + +interface PageProps { + params: Promise<{ problemId: string }>; +} + +const Page = async ({ params }: PageProps) => { + const { problemId } = await params; + + return ; +}; + +export default Page; diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx new file mode 100644 index 0000000..8a017f9 --- /dev/null +++ b/src/app/(protected)/layout.tsx @@ -0,0 +1,11 @@ +import { AdminProtectedLayout } from "@/features/admin/ui/layouts/admin-protected-layout"; + +interface LayoutProps { + children: React.ReactNode; +} + +const Layout = ({ children }: LayoutProps) => { + return {children}; +}; + +export default Layout; diff --git a/src/app/actions/ai-testcase.ts b/src/app/actions/ai-testcase.ts new file mode 100644 index 0000000..5bdabc6 --- /dev/null +++ b/src/app/actions/ai-testcase.ts @@ -0,0 +1,150 @@ +"use server"; + +import {AITestCaseInput, AITestCaseOutput, AITestCaseOutputSchema} from "@/types/ai-testcase"; + +import { deepseek } from "@/lib/ai"; +import { CoreMessage, generateText } from "ai"; +import prisma from "@/lib/prisma"; + + +/** + * + * @param input + * @returns + */ +export const generateAITestcase = async ( + input: AITestCaseInput +): Promise => { + const model = deepseek("deepseek-chat"); + + let problemDetails = ""; + + if (input.problemId) { + try { + // 尝试获取英文描述 + const problemLocalizationEn = await prisma.problemLocalization.findUnique({ + where: { + problemId_locale_type: { + problemId: input.problemId, + locale: "en", + type: "DESCRIPTION", + }, + }, + include: { + problem: true, + }, + }); + + if (problemLocalizationEn) { + problemDetails = ` +Problem Requirements: +------------------- +Description: ${problemLocalizationEn.content} + `; + } else { + // 回退到中文描述 + const problemLocalizationZh = await prisma.problemLocalization.findUnique({ + where: { + problemId_locale_type: { + problemId: input.problemId, + locale: "zh", + type: "DESCRIPTION", + }, + }, + include: { + problem: true, + }, + }); + + if (problemLocalizationZh) { + problemDetails = ` +Problem Requirements: +------------------- +Description: ${problemLocalizationZh.content} + `; + console.warn(`Fallback to Chinese description for problemId: ${input.problemId}`); + } else { + problemDetails = "Problem description not found in any language."; + console.warn(`No description found for problemId: ${input.problemId}`); + } + } + } catch (error) { + console.error("Failed to fetch problem details:", error); + problemDetails = "Error fetching problem description."; + } + } + + + + // 构建AI提示词 + const prompt = ` +Analyze the problem statement to get the expected input structure, constraints, and output logic. Generate **novel, randomized** inputs/outputs that strictly adhere to the problem's requirements. Focus on: +Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers. + +1. **Input Data Structure**: Identify required formats (e.g., arrays, integers, strings). +2. **Input Constraints**: Determine valid ranges (e.g., array length: 2–100, integers: -1000 to 1000) and edge cases. +3. **Output Logic**: Ensure outputs correctly reflect problem-specific operations. +4. **Randomization**: + Vary input magnitudes (mix min/max/-edge values with mid-range randomness) + Use diverse data distributions (e.g., sorted/unsorted, negative/positive values) + Avoid patterns from existing examples + +Your entire response/output is going to consist of a single JSON object {}, and you will NOT wrap it within JSON Markdown markers. + +Here is the problem description: + +${problemDetails} + +Respond **ONLY** with this JSON structure. +***Do not wrap the json codes in JSON markers*** : +{ + "expectedOutput": "Randomized output (e.g., [-5, 100] instead of [1, 2])", + "inputs": [ + { + "name": "Parameter 1", + "value": // Use string to wrap actual JSON types (arrays/numbers/strings) + }, + ... // Add parameters as needed + ] +} + + +`; + + // 发送请求给OpenAI + const messages: CoreMessage[] = [{ role: "user", content: prompt }]; + let text; + try { + const response = await generateText({ + model: model, + messages: messages, + }); + text = response.text; + } catch (error) { + console.error("Error generating text with OpenAI:", error); + throw new Error("Failed to generate response from OpenAI"); + } + + // 解析LLM响应 + let llmResponseJson; + try { + llmResponseJson = JSON.parse(text) + + + } catch (error) { + console.error("Failed to parse LLM response as JSON:", error); + console.error("LLM raw output:", text); + throw new Error("Invalid JSON response from LLM"); + } + + + // 验证响应格式 + const validationResult = AITestCaseOutputSchema.safeParse(llmResponseJson); + if (!validationResult.success) { + console.error("Zod validation failed:", validationResult.error.format()); + throw new Error("Response validation failed"); + } + + console.log("LLM response:", llmResponseJson); + return validationResult.data; +}; \ No newline at end of file diff --git a/src/components/content/pre-detail.tsx b/src/components/content/pre-detail.tsx index 2293ce7..c535fa8 100644 --- a/src/components/content/pre-detail.tsx +++ b/src/components/content/pre-detail.tsx @@ -1,13 +1,13 @@ "use client"; import { cn } from "@/lib/utils"; +import { Actions } from "flexlayout-react"; import { Button } from "@/components/ui/button"; import { ReactNode, useRef, useState } from "react"; import { CheckIcon, CopyIcon, RepeatIcon } from "lucide-react"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { useProblemEditorStore } from "@/stores/problem-editor"; -import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout"; -import { Actions } from "flexlayout-react"; +import { useProblemFlexLayoutStore } from "@/stores/flexlayout"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; interface PreDetailProps { children?: ReactNode; diff --git a/src/components/creater/edit-testcase-panel.tsx b/src/components/creater/edit-testcase-panel.tsx index 81ec644..7c83cd5 100644 --- a/src/components/creater/edit-testcase-panel.tsx +++ b/src/components/creater/edit-testcase-panel.tsx @@ -1,180 +1,240 @@ "use client"; -import { useState, useEffect } from "react"; -import { Label } from "@/components/ui/label"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { getProblemData } from "@/app/actions/getProblem"; +import {useState, useEffect} from "react"; +import {generateAITestcase} from "@/app/actions/ai-testcase"; +import {Label} from "@/components/ui/label"; +import {Input} from "@/components/ui/input"; +import {Button} from "@/components/ui/button"; +import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; +import {getProblemData} from "@/app/actions/getProblem"; export default function EditTestcasePanel({ - problemId, + problemId, }: { - problemId: string; + problemId: string; }) { - const [testcases, setTestcases] = useState< - Array<{ - id: string; - expectedOutput: string; - inputs: Array<{ - name: string; - value: string; - }>; - }> - >([]); + const [testcases, setTestcases] = useState< + Array<{ + id: string; + expectedOutput: string; + inputs: Array<{ + name: string; + value: string; + }>; + }> + >([]); - useEffect(() => { - async function fetchData() { - try { - const problemData = await getProblemData(problemId); - if (problemData && problemData.testcases) { - setTestcases(problemData.testcases); - } else { - setTestcases([]); + useEffect(() => { + async function fetchData() { + try { + const problemData = await getProblemData(problemId); + if (problemData && problemData.testcases) { + setTestcases(problemData.testcases); + } else { + setTestcases([]); + } + } catch (error) { + console.error("加载测试用例失败:", error); + setTestcases([]); + } + } + + fetchData(); + }, [problemId]); + + const handleAddTestcase = () => { + setTestcases([ + ...testcases, + { + id: `new-${Date.now()}`, + expectedOutput: "", + inputs: [{name: "input1", value: ""}], + }, + ]); + }; + + const [isGenerating, setIsGenerating] = useState(false); + + const handleAITestcase = async () => { + setIsGenerating(true); + try { + const AIOutputParsed = await generateAITestcase({problemId: problemId}); + setTestcases([ + ...testcases, + { + id: `new-${Date.now()}`, + expectedOutput: AIOutputParsed.expectedOutput, + inputs: AIOutputParsed.inputs + } + ]) + window.scrollTo({ + top: document.body.scrollHeight, + behavior: 'smooth', + }); + } catch (error) { + console.error(error) + } finally { + setIsGenerating(false); } - } catch (error) { - console.error("加载测试用例失败:", error); - setTestcases([]); - } } - fetchData(); - }, [problemId]); - const handleAddTestcase = () => { - setTestcases([ - ...testcases, - { - id: `new-${Date.now()}`, - expectedOutput: "", - inputs: [{ name: "input1", value: "" }], - }, - ]); - }; + const handleRemoveTestcase = (index: number) => { + const newTestcases = [...testcases]; + newTestcases.splice(index, 1); + setTestcases(newTestcases); + }; - const handleRemoveTestcase = (index: number) => { - const newTestcases = [...testcases]; - newTestcases.splice(index, 1); - setTestcases(newTestcases); - }; + const handleInputChange = ( + testcaseIndex: number, + inputIndex: number, + field: "name" | "value", + value: string + ) => { + const newTestcases = [...testcases]; + newTestcases[testcaseIndex].inputs[inputIndex][field] = value; + setTestcases(newTestcases); + }; - const handleInputChange = ( - testcaseIndex: number, - inputIndex: number, - field: "name" | "value", - value: string - ) => { - const newTestcases = [...testcases]; - newTestcases[testcaseIndex].inputs[inputIndex][field] = value; - setTestcases(newTestcases); - }; + const handleExpectedOutputChange = (testcaseIndex: number, value: string) => { + const newTestcases = [...testcases]; + newTestcases[testcaseIndex].expectedOutput = value; + setTestcases(newTestcases); + }; - const handleExpectedOutputChange = (testcaseIndex: number, value: string) => { - const newTestcases = [...testcases]; - newTestcases[testcaseIndex].expectedOutput = value; - setTestcases(newTestcases); - }; + const handleAddInput = (testcaseIndex: number) => { + const newTestcases = [...testcases]; + newTestcases[testcaseIndex].inputs.push({ + name: `input${newTestcases[testcaseIndex].inputs.length + 1}`, + value: "", + }); + setTestcases(newTestcases); + }; - const handleAddInput = (testcaseIndex: number) => { - const newTestcases = [...testcases]; - newTestcases[testcaseIndex].inputs.push({ - name: `input${newTestcases[testcaseIndex].inputs.length + 1}`, - value: "", - }); - setTestcases(newTestcases); - }; + const handleRemoveInput = (testcaseIndex: number, inputIndex: number) => { + const newTestcases = [...testcases]; + newTestcases[testcaseIndex].inputs.splice(inputIndex, 1); + setTestcases(newTestcases); + }; - const handleRemoveInput = (testcaseIndex: number, inputIndex: number) => { - const newTestcases = [...testcases]; - newTestcases[testcaseIndex].inputs.splice(inputIndex, 1); - setTestcases(newTestcases); - }; - - return ( - - - 测试用例 - - - -
- {testcases.map((testcase, index) => ( -
-
-

测试用例 {index + 1}

+ return ( + + + 测试用例 +
{/* space-x-1 让按钮更接近 */} + -
+
+ + +
+ {testcases.map((testcase, index) => ( +
+
+

测试用例 {index + 1}

+ +
-
- - handleExpectedOutputChange(index, e.target.value)} - placeholder="输入预期输出" - /> -
+
+ + handleExpectedOutputChange(index, e.target.value)} + placeholder="输入预期输出" + /> +
-
-
- - -
+
+
+ + +
- {testcase.inputs.map((input, inputIndex) => ( -
-
- - - handleInputChange(index, inputIndex, "name", e.target.value) - } - placeholder="输入参数名称" - /> -
-
- - - handleInputChange(index, inputIndex, "value", e.target.value) - } - placeholder="输入参数值" - /> -
- {inputIndex > 0 && ( - - )} + {testcase.inputs.map((input, inputIndex) => ( +
+
+ + + handleInputChange(index, inputIndex, "name", e.target.value) + } + placeholder="输入参数名称" + /> +
+
+ + + handleInputChange(index, inputIndex, "value", e.target.value) + } + placeholder="输入参数值" + /> +
+ {inputIndex > 0 && ( + + )} +
+ ))} +
))} -
- ))} -
-
- - ); + + + + ); } diff --git a/src/features/admin/ui/components/problem-edit-flexlayout.tsx b/src/features/admin/ui/components/problem-edit-flexlayout.tsx new file mode 100644 index 0000000..90a45d9 --- /dev/null +++ b/src/features/admin/ui/components/problem-edit-flexlayout.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { useProblemEditFlexLayoutStore } from "@/stores/flexlayout"; +import { FlexLayout } from "@/features/problems/components/flexlayout"; + +interface ProblemEditFlexLayoutProps { + components: Record; +} + +export const ProblemEditFlexLayout = ({ + components, +}: ProblemEditFlexLayoutProps) => { + const { hasHydrated, model, jsonModel, setModel, setJsonModel } = + useProblemEditFlexLayoutStore(); + + return ( + + ); +}; diff --git a/src/features/admin/ui/layouts/admin-protected-layout.tsx b/src/features/admin/ui/layouts/admin-protected-layout.tsx new file mode 100644 index 0000000..5af0b59 --- /dev/null +++ b/src/features/admin/ui/layouts/admin-protected-layout.tsx @@ -0,0 +1,30 @@ +import prisma from "@/lib/prisma"; +import { auth, signIn } from "@/lib/auth"; +import { redirect } from "next/navigation"; + +interface AdminProtectedLayoutProps { + children: React.ReactNode; +} + +export const AdminProtectedLayout = async ({ + children, +}: AdminProtectedLayoutProps) => { + const session = await auth(); + const userId = session?.user?.id; + if (!userId) { + await signIn(); + } + + const user = await prisma.user.findUnique({ + select: { + role: true, + }, + where: { + id: userId, + }, + }); + + if (user?.role !== "ADMIN") redirect("/unauthorized"); + + return <>{children}; +}; diff --git a/src/features/admin/ui/layouts/problem-edit-layout.tsx b/src/features/admin/ui/layouts/problem-edit-layout.tsx new file mode 100644 index 0000000..a1857cb --- /dev/null +++ b/src/features/admin/ui/layouts/problem-edit-layout.tsx @@ -0,0 +1,29 @@ +import { Suspense } from "react"; +import { BackButton } from "@/components/back-button"; +import { UserAvatar, UserAvatarSkeleton } from "@/components/user-avatar"; + +interface ProblemEditLayoutProps { + children: React.ReactNode; +} + +export const ProblemEditLayout = ({ children }: ProblemEditLayoutProps) => { + return ( +
+
+
+
+ +
+
+ }> + + +
+
+
+
+ {children} +
+
+ ); +}; diff --git a/src/features/admin/ui/views/problem-edit-view.tsx b/src/features/admin/ui/views/problem-edit-view.tsx new file mode 100644 index 0000000..add476f --- /dev/null +++ b/src/features/admin/ui/views/problem-edit-view.tsx @@ -0,0 +1,26 @@ +import EditCodePanel from "@/components/creater/edit-code-panel"; +import EditDetailPanel from "@/components/creater/edit-detail-panel"; +import EditSolutionPanel from "@/components/creater/edit-solution-panel"; +import EditTestcasePanel from "@/components/creater/edit-testcase-panel"; +import EditDescriptionPanel from "@/components/creater/edit-description-panel"; +import { ProblemEditFlexLayout } from "@/features/admin/ui/components/problem-edit-flexlayout"; + +interface ProblemEditViewProps { + problemId: string; +} + +export const ProblemEditView = ({ problemId }: ProblemEditViewProps) => { + const components: Record = { + description: , + solution: , + detail: , + code: , + testcase: , + }; + + return ( +
+ +
+ ); +}; diff --git a/src/features/problems/bot/components/view-bot-button.tsx b/src/features/problems/bot/components/view-bot-button.tsx index e2abb06..e4ae598 100644 --- a/src/features/problems/bot/components/view-bot-button.tsx +++ b/src/features/problems/bot/components/view-bot-button.tsx @@ -11,7 +11,7 @@ import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import { Toggle } from "@/components/ui/toggle"; import { Actions, DockLocation } from "flexlayout-react"; -import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout"; +import { useProblemFlexLayoutStore } from "@/stores/flexlayout"; export const ViewBotButton = () => { const t = useTranslations(); diff --git a/src/features/problems/components/flexlayout.tsx b/src/features/problems/components/flexlayout.tsx new file mode 100644 index 0000000..2996a6a --- /dev/null +++ b/src/features/problems/components/flexlayout.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { + BotIcon, + CircleCheckBigIcon, + FileTextIcon, + FlaskConicalIcon, + SquareCheckIcon, + SquarePenIcon, +} from "lucide-react"; +import { + IJsonModel, + ITabRenderValues, + Layout, + Model, + TabNode, +} from "flexlayout-react"; +import "@/styles/flexlayout.css"; +import { useTranslations } from "next-intl"; +import { useCallback, useEffect } from "react"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface FlexLayoutProps { + components: Record; + hasHydrated: boolean; + model: Model | null; + jsonModel: IJsonModel; + setModel: (model: Model) => void; + setJsonModel: (jsonModel: IJsonModel) => void; +} + +export const FlexLayout = ({ + components, + hasHydrated, + model, + jsonModel, + setModel, + setJsonModel, +}: FlexLayoutProps) => { + const t = useTranslations("ProblemPage"); + + useEffect(() => { + if (hasHydrated && !model) { + const model = Model.fromJson(jsonModel); + setModel(model); + } + }, [hasHydrated, jsonModel, model, setModel]); + + const onModelChange = useCallback( + (model: Model) => { + const jsonModel = model.toJson(); + setJsonModel(jsonModel); + }, + [setJsonModel] + ); + + const factory = useCallback( + (node: TabNode) => { + const component = node.getComponent(); + return component ? components[component] : null; + }, + [components] + ); + + const onRenderTab = useCallback( + (node: TabNode, renderValues: ITabRenderValues) => { + const Icon = getIconForTab(node.getId()); + renderValues.leading = Icon ? ( +