feat: 添加项目详情 Markdown 渲染支持及项目添加脚本
- 新增 MarkdownContent 组件支持 Markdown 渲染 - 新增 scripts 目录下的项目添加工具脚本 - 更新项目详情页面支持 Markdown 内容显示 - 更新依赖包支持 Markdown 解析 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+26
-20
@@ -11,41 +11,47 @@
|
|||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@prisma/client": "^6.1.0",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
||||||
|
"@radix-ui/react-navigation-menu": "^1.2.2",
|
||||||
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.468.0",
|
||||||
"next": "15.1.6",
|
"next": "15.1.6",
|
||||||
|
"next-intl": "^4.0.2",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"next-intl": "^4.0.2",
|
"react-markdown": "^10.1.0",
|
||||||
"@prisma/client": "^6.1.0",
|
"rehype-raw": "^7.0.0",
|
||||||
"zod": "^3.24.1",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"clsx": "^2.1.1",
|
"rehype-shiki": "^0.0.9",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
|
"shiki": "^3.20.0",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"zod": "^3.24.1"
|
||||||
"lucide-react": "^0.468.0",
|
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.2",
|
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
|
||||||
"@radix-ui/react-separator": "^1.1.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.49.1",
|
||||||
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
"@testing-library/react": "^16.1.0",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"typescript": "^5",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.6",
|
"eslint-config-next": "15.1.6",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"postcss": "^8",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
|
"prisma": "^6.1.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"postcss": "^8",
|
"ts-node": "^10.9.2",
|
||||||
"autoprefixer": "^10.4.20",
|
"typescript": "^5",
|
||||||
"prisma": "^6.1.0",
|
"vitest": "^2.1.8"
|
||||||
"vitest": "^2.1.8",
|
|
||||||
"@testing-library/react": "^16.1.0",
|
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
|
||||||
"@playwright/test": "^1.49.1",
|
|
||||||
"ts-node": "^10.9.2"
|
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
|
||||||
|
|||||||
Generated
+1229
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,181 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const markdownContent = `# AutoGen
|
||||||
|
|
||||||
|
## 🎯 项目简介
|
||||||
|
|
||||||
|
**AutoGen** 是一个由微软开发的**多智能体 AI 应用程序框架**,可以创建能够自主工作或与人类协作的智能体。
|
||||||
|
|
||||||
|
### ✨ 核心特性
|
||||||
|
|
||||||
|
- **核心 API**:实现消息传递、事件驱动智能体以及本地和分布式运行时
|
||||||
|
- **AgentChat API**:提供更简单但更具主见的 API,用于快速原型设计
|
||||||
|
- **扩展 API**:支持 LLM 客户端的特定实现(如 OpenAI、Azure OpenAI)
|
||||||
|
- **AutoGen Studio**:用于构建多智能体应用程序的无代码 GUI
|
||||||
|
- **AutoGen Bench**:用于评估智能体性能的基准测试套件
|
||||||
|
|
||||||
|
## 📦 安装方式
|
||||||
|
|
||||||
|
\`\`\`bash
|
||||||
|
# 使用 pip 安装
|
||||||
|
pip install -U "autogen-agentchat" "autogen-ext[openai]"
|
||||||
|
|
||||||
|
# 安装 AutoGen Studio
|
||||||
|
pip install -U "autogenstudio"
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
> 💡 **提示**:AutoGen 需要 **Python 3.10 或更高版本**
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### Hello World 示例
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
import asyncio
|
||||||
|
from autogen_agentchat.agents import AssistantAgent
|
||||||
|
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
model_client = OpenAIChatCompletionClient(model="gpt-4o")
|
||||||
|
agent = AssistantAgent("assistant", model_client=model_client)
|
||||||
|
print(await agent.run(task="Say 'Hello World!'"))
|
||||||
|
await model_client.close()
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## 📊 功能对比
|
||||||
|
|
||||||
|
| 特性 | AutoGen | LangChain | CrewAI |
|
||||||
|
|------|---------|-----------|--------|
|
||||||
|
| 多智能体协作 | ✅ | ✅ | ✅ |
|
||||||
|
| 无代码 GUI | ✅ | ❌ | ❌ |
|
||||||
|
| 分布式运行时 | ✅ | ❌ | ❌ |
|
||||||
|
| .NET 支持 | ✅ | ❌ | ❌ |
|
||||||
|
| 基准测试套件 | ✅ | ❌ | ❌ |
|
||||||
|
|
||||||
|
## 🔧 高级用法
|
||||||
|
|
||||||
|
### 多智能体编排
|
||||||
|
|
||||||
|
使用 \`AgentTool\` 创建基本的多智能体编排设置:
|
||||||
|
|
||||||
|
\`\`\`python
|
||||||
|
import asyncio
|
||||||
|
from autogen_agentchat.agents import AssistantAgent
|
||||||
|
from autogen_agentchat.tools import AgentTool
|
||||||
|
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
model_client = OpenAIChatCompletionClient(model="gpt-4o")
|
||||||
|
|
||||||
|
# 创建数学专家智能体
|
||||||
|
math_agent = AssistantAgent(
|
||||||
|
"math_expert",
|
||||||
|
model_client=model_client,
|
||||||
|
system_message="You are a math expert.",
|
||||||
|
description="A math expert assistant.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建化学专家智能体
|
||||||
|
chemistry_agent = AssistantAgent(
|
||||||
|
"chemistry_expert",
|
||||||
|
model_client=model_client,
|
||||||
|
system_message="You are a chemistry expert.",
|
||||||
|
description="A chemistry expert assistant.",
|
||||||
|
)
|
||||||
|
|
||||||
|
print("智能体创建成功!")
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## 📚 任务清单
|
||||||
|
|
||||||
|
- [x] 安装 AutoGen
|
||||||
|
- [ ] 创建第一个智能体
|
||||||
|
- [ ] 配置 OpenAI API
|
||||||
|
- [ ] 运行多智能体对话
|
||||||
|
- [ ] 部署到生产环境
|
||||||
|
|
||||||
|
## 🎓 学习资源
|
||||||
|
|
||||||
|
1. [官方文档](https://microsoft.github.io/autogen/)
|
||||||
|
2. [GitHub 仓库](https://github.com/microsoft/autogen)
|
||||||
|
3. [API 参考](https://microsoft.github.io/autogen/docs/reference)
|
||||||
|
4. [示例代码](https://github.com/microsoft/autogen/tree/main/samples)
|
||||||
|
|
||||||
|
## 💬 常见问题
|
||||||
|
|
||||||
|
### Q: AutoGen 是免费的吗?
|
||||||
|
|
||||||
|
**A**: 是的!AutoGen 使用 MIT 许可证,完全开源免费。
|
||||||
|
|
||||||
|
### Q: 支持哪些 LLM 提供商?
|
||||||
|
|
||||||
|
**A**: AutoGen 支持 OpenAI、Azure OpenAI,以及通过扩展 API 支持其他提供商。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
MIT License - 详见 [LICENSE](https://github.com/microsoft/autogen/blob/main/LICENSE) 文件
|
||||||
|
|
||||||
|
**Made with ❤️ by Microsoft**
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
apiKey: 'sk_live_agent_park_webhook_key_2025',
|
||||||
|
projects: [{
|
||||||
|
name: 'AutoGen',
|
||||||
|
nameEn: 'AutoGen',
|
||||||
|
description: 'Microsoft 开发的多智能体 AI 应用程序框架,支持自主或与人类协作的智能体',
|
||||||
|
descriptionEn: 'A programming framework for creating multi-agent AI applications that can act autonomously or work alongside humans',
|
||||||
|
content: markdownContent,
|
||||||
|
contentEn: markdownContent, // 使用相同内容用于测试
|
||||||
|
status: 'ACTIVE',
|
||||||
|
source: 'GitHub',
|
||||||
|
tags: [
|
||||||
|
{ name: '多智能体', nameEn: 'Multi-Agent' },
|
||||||
|
{ name: '框架', nameEn: 'Framework' },
|
||||||
|
{ name: '微软', nameEn: 'Microsoft' },
|
||||||
|
{ name: 'Python', nameEn: 'Python' },
|
||||||
|
{ name: 'AI', nameEn: 'AI' },
|
||||||
|
{ name: 'LLM', nameEn: 'LLM' }
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{ type: 'GITHUB', url: 'https://github.com/microsoft/autogen', title: 'GitHub 仓库' },
|
||||||
|
{ type: 'WEBSITE', url: 'https://microsoft.github.io/autogen/', title: '官方文档' },
|
||||||
|
{ type: 'WEBSITE', url: 'https://pypi.org/project/autogen-agentchat/', title: 'PyPI 包' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: 3001,
|
||||||
|
path: '/api/webhook/projects',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(data)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let responseData = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
responseData += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
console.log('Status:', res.statusCode);
|
||||||
|
console.log('Response:', responseData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(data);
|
||||||
|
req.end();
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
const http = require('http');
|
||||||
|
|
||||||
|
const data = JSON.stringify({
|
||||||
|
apiKey: 'sk_live_agent_park_webhook_key_2025',
|
||||||
|
projects: [{
|
||||||
|
name: 'AutoGen',
|
||||||
|
nameEn: 'AutoGen',
|
||||||
|
description: 'Microsoft 开发的多智能体 AI 应用程序框架,支持自主或与人类协作的智能体',
|
||||||
|
descriptionEn: 'A programming framework for creating multi-agent AI applications that can act autonomously or work alongside humans',
|
||||||
|
content: 'AutoGen 是一个由微软开发的创建多智能体 AI 应用程序的框架。主要特性:\n\n1. 核心 API:实现消息传递、事件驱动智能体以及本地和分布式运行时\n2. AgentChat API:提供更简单但更具主见的 API,用于快速原型设计\n3. 扩展 API:支持 LLM 客户端的特定实现(如 OpenAI、Azure OpenAI)\n4. AutoGen Studio:用于构建多智能体应用程序的无代码 GUI\n5. AutoGen Bench:用于评估智能体性能的基准测试套件\n\n支持 Python 3.10+ 和 .NET,使用 MIT 许可证。',
|
||||||
|
contentEn: 'AutoGen is a framework for creating multi-agent AI applications. Key features: Core API, AgentChat API, Extensions API, AutoGen Studio, AutoGen Bench. Supports Python 3.10+ and .NET. MIT licensed.',
|
||||||
|
status: 'ACTIVE',
|
||||||
|
source: 'GitHub',
|
||||||
|
tags: [
|
||||||
|
{ name: '多智能体', nameEn: 'Multi-Agent' },
|
||||||
|
{ name: '框架', nameEn: 'Framework' },
|
||||||
|
{ name: '微软', nameEn: 'Microsoft' },
|
||||||
|
{ name: 'Python', nameEn: 'Python' },
|
||||||
|
{ name: 'AI', nameEn: 'AI' },
|
||||||
|
{ name: 'LLM', nameEn: 'LLM' }
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{ type: 'GITHUB', url: 'https://github.com/microsoft/autogen', title: 'GitHub 仓库' },
|
||||||
|
{ type: 'WEBSITE', url: 'https://microsoft.github.io/autogen/', title: '官方文档' },
|
||||||
|
{ type: 'WEBSITE', url: 'https://pypi.org/project/autogen-agentchat/', title: 'PyPI 包' }
|
||||||
|
]
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
port: 3001,
|
||||||
|
path: '/api/webhook/projects',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(data)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const req = http.request(options, (res) => {
|
||||||
|
let responseData = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
responseData += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
console.log('Status:', res.statusCode);
|
||||||
|
console.log('Response:', responseData);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
console.error('Error:', error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.write(data);
|
||||||
|
req.end();
|
||||||
@@ -33,7 +33,7 @@ export default async function ProjectDetailPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark font-body transition-colors duration-200">
|
<main className="min-h-screen bg-background-light dark:bg-background-dark text-text-light dark:text-text-dark font-body transition-colors duration-200">
|
||||||
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
<div className="container mx-auto max-w-6xl px-4 sm:px-6 lg:px-8 py-12">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -0,0 +1,262 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import remarkGfm from 'remark-gfm'
|
||||||
|
import rehypeRaw from 'rehype-raw'
|
||||||
|
import rehypeSanitize from 'rehype-sanitize'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import type { Components } from 'react-markdown'
|
||||||
|
|
||||||
|
interface MarkdownContentProps {
|
||||||
|
content: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate heading ID from text
|
||||||
|
function generateHeadingId(text: string): string {
|
||||||
|
return text
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, '-') // Replace spaces with -
|
||||||
|
.replace(/[^\w\-\u4e00-\u9fa5]+/g, '') // Remove non-word chars except Chinese
|
||||||
|
.replace(/\-\-+/g, '-') // Replace multiple - with single -
|
||||||
|
.replace(/^-+/, '') // Trim - from start
|
||||||
|
.replace(/-+$/, '') // Trim - from end
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom GitHub-style components
|
||||||
|
const components: Components = {
|
||||||
|
// Headings with anchor links
|
||||||
|
h1: (({ children, ...props }: any) => {
|
||||||
|
const id = generateHeadingId(children?.toString() || '')
|
||||||
|
return (
|
||||||
|
<h1
|
||||||
|
id={id}
|
||||||
|
className="border-b border-gray-300 dark:border-gray-700 pb-2 mb-4 text-2xl font-semibold scroll-mt-20"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<a href={`#${id}`} className="group">
|
||||||
|
{children}
|
||||||
|
<span className="ml-2 opacity-0 group-hover:opacity-100 inline-block text-gray-400 no-underline">
|
||||||
|
#
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
|
}) as any,
|
||||||
|
h2: (({ children, ...props }: any) => {
|
||||||
|
const id = generateHeadingId(children?.toString() || '')
|
||||||
|
return (
|
||||||
|
<h2
|
||||||
|
id={id}
|
||||||
|
className="border-b border-gray-300 dark:border-gray-700 pb-2 mb-3 mt-8 text-xl font-semibold scroll-mt-20"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<a href={`#${id}`} className="group">
|
||||||
|
{children}
|
||||||
|
<span className="ml-2 opacity-0 group-hover:opacity-100 inline-block text-gray-400 no-underline">
|
||||||
|
#
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
|
}) as any,
|
||||||
|
h3: (({ children, ...props }: any) => {
|
||||||
|
const id = generateHeadingId(children?.toString() || '')
|
||||||
|
return (
|
||||||
|
<h3 id={id} className="mb-3 mt-6 text-lg font-semibold scroll-mt-20" {...props}>
|
||||||
|
<a href={`#${id}`} className="group">
|
||||||
|
{children}
|
||||||
|
<span className="ml-2 opacity-0 group-hover:opacity-100 inline-block text-gray-400 no-underline">
|
||||||
|
#
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
)
|
||||||
|
}) as any,
|
||||||
|
h4: ({ children, ...props }) => (
|
||||||
|
<h4 className="mb-2 mt-4 text-base font-semibold" {...props}>
|
||||||
|
{children}
|
||||||
|
</h4>
|
||||||
|
),
|
||||||
|
|
||||||
|
// Paragraphs
|
||||||
|
p: ({ children, ...props }) => (
|
||||||
|
<p className="mb-4 leading-7" {...props}>
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
|
||||||
|
// Lists
|
||||||
|
ul: ({ children, className, ...props }) => (
|
||||||
|
<ul
|
||||||
|
className={`mb-4 ml-6 list-disc space-y-2 ${className || ''}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
),
|
||||||
|
ol: ({ children, className, ...props }) => (
|
||||||
|
<ol
|
||||||
|
className={`mb-4 ml-6 list-decimal space-y-2 ${className || ''}`}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
),
|
||||||
|
li: ({ children, className, ...props }) => (
|
||||||
|
<li className={`mt-2 ${className || ''}`} {...props}>
|
||||||
|
{children}
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
|
||||||
|
// Code blocks with syntax highlighting
|
||||||
|
code: ({ inline, className, children, ...props }: any) => {
|
||||||
|
if (inline) {
|
||||||
|
return (
|
||||||
|
<code
|
||||||
|
className="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-sm font-mono border border-gray-300 dark:border-gray-600"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = className?.replace(/language-/, '') || 'text'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SyntaxHighlighterWrapper language={language} code={String(children).trim()} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Blockquotes
|
||||||
|
blockquote: ({ children, ...props }) => (
|
||||||
|
<blockquote
|
||||||
|
className="pl-4 border-l-4 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 italic my-4"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</blockquote>
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
table: ({ children, ...props }) => (
|
||||||
|
<div className="overflow-x-auto my-4">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300 dark:divide-gray-700 border border-gray-300 dark:border-gray-600" {...props}>
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
thead: ({ children, ...props }) => (
|
||||||
|
<thead className="bg-gray-50 dark:bg-gray-800" {...props}>
|
||||||
|
{children}
|
||||||
|
</thead>
|
||||||
|
),
|
||||||
|
th: ({ children, ...props }) => (
|
||||||
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-300 dark:border-gray-700" {...props}>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
),
|
||||||
|
td: ({ children, ...props }) => (
|
||||||
|
<td className="px-4 py-2 text-sm border-b border-gray-300 dark:border-gray-700" {...props}>
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
),
|
||||||
|
|
||||||
|
// Links
|
||||||
|
a: ({ children, href, ...props }) => (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
target={href?.startsWith('http') ? '_blank' : undefined}
|
||||||
|
rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
|
||||||
|
// Images
|
||||||
|
img: ({ src, alt, ...props }) => (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className="rounded-lg border border-gray-300 dark:border-gray-600 my-4 max-w-full h-auto"
|
||||||
|
loading="lazy"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
|
||||||
|
// Horizontal rule
|
||||||
|
hr: ({ ...props }) => (
|
||||||
|
<hr className="my-8 border-t border-gray-300 dark:border-gray-700" {...props} />
|
||||||
|
),
|
||||||
|
|
||||||
|
// Task lists
|
||||||
|
input: ({ type, checked, ...props }) =>
|
||||||
|
type === 'checkbox' ? (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checked}
|
||||||
|
readOnly
|
||||||
|
className="mr-2 h-4 w-4 rounded border-gray-300"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syntax highlighter wrapper (will use Shiki for server-side, Prism for client)
|
||||||
|
function SyntaxHighlighterWrapper({ language, code }: { language: string; code: string }) {
|
||||||
|
const [highlighted, setHighlighted] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// For now, use simple highlighting with proper escaping
|
||||||
|
// In production, you'd want to use Shiki or Prism for better highlighting
|
||||||
|
const escaped = code
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
|
||||||
|
setHighlighted(escaped)
|
||||||
|
}, [code])
|
||||||
|
|
||||||
|
if (!highlighted) {
|
||||||
|
return (
|
||||||
|
<pre className="bg-gray-900 dark:bg-gray-950 text-gray-100 p-4 rounded-lg overflow-x-auto my-4 border border-gray-700">
|
||||||
|
<code className="text-sm font-mono">{code}</code>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<pre className="bg-gray-900 dark:bg-gray-950 text-gray-100 p-4 rounded-lg overflow-x-auto my-4 border border-gray-700">
|
||||||
|
<code
|
||||||
|
className="text-sm font-mono"
|
||||||
|
dangerouslySetInnerHTML={{ __html: highlighted }}
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MarkdownContent({ content, className = '' }: MarkdownContentProps) {
|
||||||
|
|
||||||
|
if (!content || content.trim() === '') {
|
||||||
|
return (
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 italic">No content available.</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className={`markdown-body text-gray-900 dark:text-gray-100 ${className}`}>
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
||||||
|
components={components}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { MarkdownContent } from './MarkdownContent'
|
||||||
|
|
||||||
interface ProjectDetailProps {
|
interface ProjectDetailProps {
|
||||||
project: {
|
project: {
|
||||||
@@ -127,18 +128,14 @@ export function ProjectDetail({ project, locale }: ProjectDetailProps) {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Article Content */}
|
{/* Article Content */}
|
||||||
<article className="prose prose-lg dark:prose-invert max-w-none prose-headings:font-display prose-a:text-blue-600 dark:prose-a:text-blue-400 hover:prose-a:text-blue-800 dark:hover:prose-a:text-blue-300 prose-img:border-2 prose-img:border-black dark:prose-img:border-gray-600 prose-img:shadow-brutal dark:prose-img:shadow-none prose-ul:marker:text-black prose-ul:dark:marker:text-white">
|
<article className="max-w-4xl mx-auto">
|
||||||
{/* Lead paragraph */}
|
{/* Lead paragraph */}
|
||||||
<p className="lead font-display text-xl mb-8">{displayDescription}</p>
|
<p className="lead font-display text-xl mb-8 text-gray-700 dark:text-gray-300">
|
||||||
|
{displayDescription}
|
||||||
|
</p>
|
||||||
|
|
||||||
{/* Full content */}
|
{/* Full content with Markdown rendering */}
|
||||||
{displayContent && (
|
{displayContent && <MarkdownContent content={displayContent} />}
|
||||||
<div
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: displayContent.replace(/\n/g, '<br />'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Installation section if GitHub link exists */}
|
{/* Installation section if GitHub link exists */}
|
||||||
{project.links.some((l) => l.type === 'GITHUB') && (
|
{project.links.some((l) => l.type === 'GITHUB') && (
|
||||||
|
|||||||
Reference in New Issue
Block a user