Compare commits

...
2 Commits
Author SHA1 Message Date
wonder c0414e7548 ✨Feat: 完成前端郭德纲教学卡片 2026-03-27 20:30:49 +08:00
wonder 541ca96fc6 ✨Feat: 完成前端知识卡片 2026-03-27 20:01:56 +08:00
6 changed files with 310 additions and 94 deletions
+13 -2
View File
@@ -5,10 +5,11 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"strings"
"github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/prompts"
"github.com/tmc/langchaingo/outputparser" "github.com/tmc/langchaingo/outputparser"
"github.com/tmc/langchaingo/prompts"
) )
func Call(llm llms.Model, ctx context.Context) (response string) { func Call(llm llms.Model, ctx context.Context) (response string) {
@@ -96,12 +97,22 @@ func CallWithAgentDefined[T any](llm llms.Model, ctx context.Context, topic stri
responseString, err := llms.GenerateFromSinglePrompt( responseString, err := llms.GenerateFromSinglePrompt(
ctx, ctx,
llm, llm,
"用户正在学习Go语言,你是用户的助手。\n"+prompt, "用户正在学习Go语言和React框架,你是用户的助手。\n"+prompt,
) )
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if !strings.Contains(responseString, "```json") {
responseString = "```json\n" + responseString + "```"
}
fmt.Printf("AI: %s\n\n",responseString)
response, err = parse.Parse(responseString) response, err = parse.Parse(responseString)
if err != nil { if err != nil {
fmt.Printf("Parse err: %v", err) fmt.Printf("Parse err: %v", err)
-1
View File
@@ -4,7 +4,6 @@
3. 语气轻松诙谐、接地气,但内容要准确专业 3. 语气轻松诙谐、接地气,但内容要准确专业
4. 偶尔使用相声常用语,比如"您听好了"、"这就好比"、"想当年"、"您猜怎么着" 4. 偶尔使用相声常用语,比如"您听好了"、"这就好比"、"想当年"、"您猜怎么着"
5. 每讲完一个要点,可以用"这话怎么说呢?"来过渡 5. 每讲完一个要点,可以用"这话怎么说呢?"来过渡
6. 最后用"您记住了吗?下次要是有人问起来,您就把这么一通摆出来,准保让他对您刮目相看!"收尾
注意: 注意:
- 不要过度夸张,保持内容的准确性和专业性 - 不要过度夸张,保持内容的准确性和专业性
+26
View File
@@ -0,0 +1,26 @@
package utils
import (
"encoding/json"
"fmt"
"github.com/tmc/langchaingo/outputparser"
)
// Parse parses the output of an LLM call.
func (p outputparser.Defined[T)(T) Parse(text string) (T, error) {
var target T
// Removes '```json' and '```' from the start and end of the text.
const opening = "```json"
const closing = "```"
if text[:len(opening)] != opening || text[len(text)-len(closing):] != closing {
return target, nil
// return target, fmt.Errorf("input text should start with %s and end with %s", opening, closing)
}
parseableJSON := text[len(opening) : len(text)-len(closing)]
if err := json.Unmarshal([]byte(parseableJSON), &target); err != nil {
return target, fmt.Errorf("could not parse generated JSON: %w", err)
}
return target, nil
}
+4 -2
View File
@@ -1,14 +1,16 @@
import Card from "./components/Card"; import Card from "./components/Card";
import Guodegang from "./components/Guodegang";
import { Surface } from "@heroui/react"; import { Surface } from "@heroui/react";
function App() { function App() {
return ( return (
<> <>
<Surface <Surface
className="flex min-w-[320px] flex-col gap-3 rounded-3xl p-6" className="flex min-w-[320px] flex-row gap-4 rounded-3xl p-6 flex-wrap justify-center"
variant="default" variant="default"
> >
<Card /> <Card topic="React Hooks" />
<Guodegang topic="React Hooks" />
</Surface> </Surface>
</> </>
); );
+173 -79
View File
@@ -1,141 +1,235 @@
import { Card } from "@heroui/react"; import { Card } from "@heroui/react";
// import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function Default() { interface KeyPointItem {
// Data point: string;
// const [data, setData] = useState(null); description: string;
// const [loading, setLoading] = useState(true); }
// Load Data interface MisconceptionItem {
// useEffect(() => { id: string;
// const fetchData = async () => { title: string;
// try { detail: string;
// const response = await fetch("http://localhost:8080/agent/card/你好"); fix: string;
// if (!response.ok) { }
// throw new Error("API Error");
// } interface PracticeItem {
// const result = await response.json(); id: string;
// setData(result); title: string;
// } catch (error) { detail: string;
// console.error("Failed to fetch data: ", error); correct_example?: string;
// } finally { incorrect_example?: string;
// setLoading(false); }
// }
// }; interface KeyPoints {
// fetchData(); header: string;
// }, []); items: KeyPointItem[];
}
interface CommonMisconceptions {
header: string;
items: MisconceptionItem[];
}
interface BestPractices {
header: string;
items: PracticeItem[];
}
interface Mnemonic {
header: string;
content: string;
}
interface CardData {
summary: string;
key_points: KeyPoints;
common_misconceptions: CommonMisconceptions;
best_practices: BestPractices;
mnemonic: Mnemonic;
}
interface Props {
topic: string;
}
export default function Default({ topic }: Props) {
const [data, setData] = useState<CardData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`http://localhost:8080/card/${encodeURIComponent(topic)}`,
);
if (!response.ok) {
throw new Error("API Error");
}
const result = await response.json();
setData(result.message);
setError(null);
} catch (error) {
console.error("Failed to fetch data: ", error);
setError("Failed to load data. Please try again.");
} finally {
setLoading(false);
}
};
fetchData();
}, [topic]);
if (loading) {
return (
<div className="w-full p-4 sm:p-6 md:max-w-2xl lg:max-w-4xl mx-auto">
<Card className="relative w-full max-w-2xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100 p-8">
<div className="text-center text-gray-600">Loading...</div>
</Card>
</div>
);
}
if (error) {
return (
<div className="w-full p-4 sm:p-6 md:max-w-2xl lg:max-w-4xl mx-auto">
<Card className="relative w-full max-w-2xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100 p-8">
<div className="text-center text-red-600">{error}</div>
</Card>
</div>
);
}
return ( return (
<div className="w-full p-4 sm:p-6 md:max-w-2xl lg:max-w-4xl mx-auto"> <div className="w-full p-4 sm:p-6 md:max-w-2xl lg:max-w-4xl mx-auto">
<Card className="relative w-full max-w-2xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100"> <Card className="relative w-full max-w-2xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100">
{/* 背景表情符号 - 放大、虚化、右下角 */}
<div className="absolute upper-0 right-0 -mr-2 -mb-2 pointer-events-none select-none"> <div className="absolute upper-0 right-0 -mr-2 -mb-2 pointer-events-none select-none">
<span className="text-[10rem] opacity-80 blur-none">🧐</span> <span className="text-[10rem] opacity-80 blur-none">🧐</span>
</div> </div>
{/* 内容区域 - 确保在背景之上 */}
<div className="relative z-5"> <div className="relative z-5">
{/* 标题区域 */}
<div className="px-6 pt-6 pb-4"> <div className="px-6 pt-6 pb-4">
<Card.Title className="text-2xl font-bold text-gray-800 mb-2"> <Card.Title className="text-2xl font-bold text-gray-800 mb-2">
主题 xxx {topic}
</Card.Title> </Card.Title>
<div className="h-1 w-20 bg-gradient-to-r from-blue-500 to-indigo-500 rounded"></div> <div className="h-1 w-20 bg-gradient-to-r from-blue-500 to-indigo-500 rounded"></div>
{data!.summary && (
<p className="mt-3 text-gray-600">{data!.summary}</p>
)}
</div> </div>
{/* 内容区域 */}
<Card.Description className="px-6 pb-6 space-y-6"> <Card.Description className="px-6 pb-6 space-y-6">
{/* 关键要点 */}
<div> <div>
<h3 className="flex items-center text-lg font-semibold text-gray-700 mb-3"> <h3 className="flex items-center text-lg font-semibold text-gray-700 mb-3">
<span className="mr-2">🔑</span> <span className="mr-2"></span>
关键要点 {data!.key_points.header}
</h3> </h3>
<ul className="space-y-2 ml-7"> <ul className="space-y-2 ml-7">
{[ {data!.key_points.items.map((item, index) => (
"要点1(具体说明,20字以内)",
"要点2(具体说明,20字以内)",
"要点3(具体说明,20字以内)",
"要点4(具体说明,20字以内)",
].map((item, index) => (
<li key={index} className="flex items-start"> <li key={index} className="flex items-start">
<span className="inline-flex items-center justify-center w-6 h-6 bg-blue-100 text-blue-600 rounded-full text-sm font-medium mr-2.5 mt-0.5 flex-shrink-0 min-w-[1.5rem]"> <span className="inline-flex items-center justify-center w-6 h-6 bg-blue-100 text-blue-600 rounded-full text-sm font-medium mr-2.5 mt-0.5 flex-shrink-0 min-w-[1.5rem]">
{index + 1} {index + 1}
</span> </span>
<span className="text-gray-600 pt-0.5">{item}</span> <span className="text-gray-600 pt-0.5">
<span className="font-medium text-gray-800">
{item.point}
</span>
{item.description && (
<span className="ml-1">({item.description})</span>
)}
</span>
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
{/* 常见误区 */}
<div> <div>
<h3 className="flex items-center text-lg font-semibold text-gray-700 mb-3"> <h3 className="flex items-center text-lg font-semibold text-gray-700 mb-3">
<span className="mr-2">⚠️</span> <span className="mr-2"></span>
常见误区 {data!.common_misconceptions.header}
</h3> </h3>
<ul className="space-y-2 ml-7"> <ul className="space-y-3 ml-7">
<li className="flex items-start"> {data!.common_misconceptions.items.map((item, index) => (
<li key={item.id || index} className="flex flex-start">
<span className="text-red-500 mr-2.5 mt-0.5 flex-shrink-0"> <span className="text-red-500 mr-2.5 mt-0.5 flex-shrink-0">
✗ ✗
</span> </span>
<span className="text-gray-600 pt-0.5"> <div className="flex-1">
误区1: <span className="text-gray-700 font-medium">
<span className="text-red-600 font-medium">[说明]</span> {item.title}
</span>
</li>
<li className="flex items-start">
<span className="text-red-500 mr-2.5 mt-0.5 flex-shrink-0">
✗
</span>
<span className="text-gray-600 pt-0.5">
误区2:
<span className="text-red-600 font-medium">[说明]</span>
</span> </span>
<p className="text-gray-600 text-sm mt-1">
{item.detail}
</p>
{item.fix && (
<div className="mt-2 flex items-start text-sm">
<span className="text-green-600 mr-1.5 mt-0.5"></span>
<span className="text-gray-600">
建议:{item.fix}
</span>
</div>
)}
</div>
</li> </li>
))}
</ul> </ul>
</div> </div>
{/* 最佳实践 */}
<div> <div>
<h3 className="flex items-center text-lg font-semibold text-gray-700 mb-3"> <h3 className="flex items-center text-lg font-semibold text-gray-700 mb-3">
<span className="mr-2">✅</span> <span className="mr-2"></span>
最佳实践 {data!.best_practices.header}
</h3> </h3>
<ul className="space-y-2 ml-7"> <ul className="space-y-3 ml-7">
<li className="flex items-start"> {data!.best_practices.items.map((item, index) => (
<li key={item.id || index} className="flex flex-start">
<span className="text-green-500 mr-2.5 mt-0.5 flex-shrink-0"> <span className="text-green-500 mr-2.5 mt-0.5 flex-shrink-0">
✓ ✓
</span> </span>
<span className="text-gray-600 pt-0.5"> <div className="flex-1">
实践1: <span className="text-gray-700 font-medium">
<span className="text-green-600 font-medium"> {item.title}
[具体建议]
</span>
</span>
</li>
<li className="flex items-start">
<span className="text-green-500 mr-2.5 mt-0.5 flex-shrink-0">
✓
</span>
<span className="text-gray-600 pt-0.5">
实践2:
<span className="text-green-600 font-medium">
[具体建议]
</span> </span>
<p className="text-gray-600 text-sm mt-1">
{item.detail}
</p>
{(item.correct_example || item.incorrect_example) && (
<div className="mt-2 space-y-1">
{item.incorrect_example && (
<div className="flex items-start text-sm bg-red-50 px-2 py-1.5 rounded border border-red-100">
<span className="text-red-500 mr-1.5 flex-shrink-0">
❌
</span> </span>
<span className="text-gray-700">
{item.incorrect_example}
</span>
</div>
)}
{item.correct_example && (
<div className="flex items-start text-sm bg-green-50 px-2 py-1.5 rounded border border-green-100">
<span className="text-green-500 mr-1.5 flex-shrink-0">
✅
</span>
<span className="text-gray-700">
{item.correct_example}
</span>
</div>
)}
</div>
)}
</div>
</li> </li>
))}
</ul> </ul>
</div> </div>
{/* 记忆口诀 */}
<div className="mt-6 p-4 bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl border border-purple-100"> <div className="mt-6 p-4 bg-gradient-to-r from-purple-50 to-pink-50 rounded-xl border border-purple-100">
<h3 className="flex items-center text-lg font-semibold text-purple-700 mb-2"> <h3 className="flex items-center text-lg font-semibold text-purple-700 mb-2">
<span className="mr-2">🎯</span> <span className="mr-2"></span>
记忆口诀 {data!.mnemonic.header}
</h3> </h3>
<p className="text-gray-700 italic bg-white px-4 py-3 rounded-lg shadow-sm"> <p className="text-gray-700 italic bg-white px-4 py-3 rounded-lg shadow-sm">
[朗朗上口的口诀,帮助记忆] {data!.mnemonic.content}
</p> </p>
</div> </div>
</Card.Description> </Card.Description>
+84
View File
@@ -0,0 +1,84 @@
import { Card } from "@heroui/react";
import { useEffect, useState } from "react";
interface GuodegangData {
name: string;
content: string;
}
interface Props {
topic: string;
}
export default function Guodegang({ topic }: Props) {
const [data, setData] = useState<GuodegangData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(
`http://localhost:8080/guodegang/${encodeURIComponent(topic)}`,
);
if (!response.ok) {
throw new Error("API Error");
}
const result = await response.json();
setData(result.message);
setError(null);
} catch (error) {
console.error("Failed to fetch data: ", error);
setError("Failed to load data. Please try again.");
} finally {
setLoading(false);
}
};
fetchData();
}, [topic]);
if (loading) {
return (
<div className="w-full p-4 sm:p-6 md:max-w-2xl lg:max-w-4xl mx-auto">
<Card className="relative w-full max-w-2xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100 p-8">
<div className="text-center text-gray-600">Loading...</div>
</Card>
</div>
);
}
if (error) {
return (
<div className="w-full p-4 sm:p-6 md:max-w-2xl lg:max-w-4xl mx-auto">
<Card className="relative w-full max-w-2xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100 p-8">
<div className="text-center text-red-600">{error}</div>
</Card>
</div>
);
}
return (
<div className="w-full p-4 sm:p-6 md:max-w-2xl lg:max-w-4xl mx-auto">
<Card className="relative w-full max-w-2xl mx-auto bg-white shadow-xl rounded-2xl overflow-hidden border border-gray-100">
<div className="absolute upper-0 right-0 -mr-2 -mb-2 pointer-events-none select-none">
<span className="text-[10rem] opacity-80 blur-none">🎤</span>
</div>
<div className="relative z-5">
<div className="px-6 pt-6 pb-4">
<Card.Title className="text-2xl font-bold text-gray-800 mb-2">
{data!.name}
</Card.Title>
<div className="h-1 w-20 bg-gradient-to-r from-yellow-500 to-orange-500 rounded"></div>
</div>
<Card.Description className="px-6 pb-6">
<p className="text-gray-700 leading-relaxed whitespace-pre-line">
{data!.content}
</p>
</Card.Description>
</div>
</Card>
</div>
);
}