feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)
Real IMAP connection via imapflow library: - Connects to IMAP server (IMAPFlow, TLS on port 993) - Fetches recent messages (configurable limit, default 20) - Extracts: from, fromEmail, subject, date, snippet (200 chars) - Thread-to-contact matching (PRD §6.1): 1. Match sender email against persons table 2. If no person match, extract domain and match against company.website 3. Returns matchedCompany + matchedPerson for each email - Audit-logged (action: crm.mailsync:imap) - Zod validated (host, port, user, password, folder, limit) - 502 on IMAP connection failure with detail - 400 on validation failure Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
This commit is contained in:
1 parent
7acc0c5451
commit
e45b68bb8b
1089 files changed
+260614
No files matched your search
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 z-ai-web-dev-sdk Skills
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+580
@@ -0,0 +1,580 @@
|
||||
---
|
||||
name: ASR
|
||||
description: Implement speech-to-text (ASR/automatic speech recognition) capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to transcribe audio files, convert speech to text, build voice input features, or process audio recordings. Supports base64 encoded audio files and returns accurate text transcriptions.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# ASR (Speech to Text) Skill
|
||||
|
||||
This skill guides the implementation of speech-to-text (ASR) functionality using the z-ai-web-dev-sdk package, enabling accurate transcription of spoken audio into text.
|
||||
|
||||
## Skills Path
|
||||
|
||||
**Skill Location**: `{project_path}/skills/ASR`
|
||||
|
||||
this skill is located at above path in your project.
|
||||
|
||||
**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/asr.ts` for a working example.
|
||||
|
||||
## Overview
|
||||
|
||||
Speech-to-Text (ASR - Automatic Speech Recognition) allows you to build applications that convert spoken language in audio files into written text, enabling voice-controlled interfaces, transcription services, and audio content analysis.
|
||||
|
||||
**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below.
|
||||
|
||||
## CLI Usage (For Simple Tasks)
|
||||
|
||||
For simple audio transcription tasks, you can use the z-ai CLI instead of writing code. This is ideal for quick transcriptions, testing audio files, or batch processing.
|
||||
|
||||
### Basic Transcription from File
|
||||
|
||||
```bash
|
||||
# Transcribe an audio file
|
||||
z-ai asr --file ./audio.wav
|
||||
|
||||
# Save transcription to JSON file
|
||||
z-ai asr -f ./recording.mp3 -o transcript.json
|
||||
|
||||
# Transcribe and view output
|
||||
z-ai asr --file ./interview.wav --output result.json
|
||||
```
|
||||
|
||||
### Transcription from Base64
|
||||
|
||||
```bash
|
||||
# Transcribe from base64 encoded audio
|
||||
z-ai asr --base64 "UklGRiQAAABXQVZFZm10..." -o result.json
|
||||
|
||||
# Using short option
|
||||
z-ai asr -b "base64_encoded_audio_data" -o transcript.json
|
||||
```
|
||||
|
||||
### Streaming Output
|
||||
|
||||
```bash
|
||||
# Stream transcription results
|
||||
z-ai asr -f ./audio.wav --stream
|
||||
```
|
||||
|
||||
### CLI Parameters
|
||||
|
||||
- `--file, -f <path>`: **Required** (if not using --base64) - Audio file path
|
||||
- `--base64, -b <base64>`: **Required** (if not using --file) - Base64 encoded audio
|
||||
- `--output, -o <path>`: Optional - Output file path (JSON format)
|
||||
- `--stream`: Optional - Stream the transcription output
|
||||
|
||||
### Supported Audio Formats
|
||||
|
||||
The ASR service supports various audio formats including:
|
||||
- WAV (.wav)
|
||||
- MP3 (.mp3)
|
||||
- Other common audio formats
|
||||
|
||||
### When to Use CLI vs SDK
|
||||
|
||||
**Use CLI for:**
|
||||
- Quick audio file transcriptions
|
||||
- Testing audio recognition accuracy
|
||||
- Simple batch processing scripts
|
||||
- One-off transcription tasks
|
||||
|
||||
**Use SDK for:**
|
||||
- Real-time audio transcription in applications
|
||||
- Integration with recording systems
|
||||
- Custom audio processing workflows
|
||||
- Production applications with streaming audio
|
||||
|
||||
## Basic ASR Implementation
|
||||
|
||||
### Simple Audio Transcription
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function transcribeAudio(audioFilePath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Read audio file and convert to base64
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
return response.text;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const transcription = await transcribeAudio('./audio.wav');
|
||||
console.log('Transcription:', transcription);
|
||||
```
|
||||
|
||||
### Transcribe Multiple Audio Files
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function transcribeBatch(audioFilePaths) {
|
||||
const zai = await ZAI.create();
|
||||
const results = [];
|
||||
|
||||
for (const filePath of audioFilePaths) {
|
||||
try {
|
||||
const audioFile = fs.readFileSync(filePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
results.push({
|
||||
file: filePath,
|
||||
success: true,
|
||||
transcription: response.text
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
file: filePath,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const files = ['./interview1.wav', './interview2.wav', './interview3.wav'];
|
||||
const transcriptions = await transcribeBatch(files);
|
||||
|
||||
transcriptions.forEach(result => {
|
||||
if (result.success) {
|
||||
console.log(`${result.file}: ${result.transcription}`);
|
||||
} else {
|
||||
console.error(`${result.file}: Error - ${result.error}`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Audio File Processing with Metadata
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function transcribeWithMetadata(audioFilePath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Get file metadata
|
||||
const stats = fs.statSync(audioFilePath);
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
const endTime = Date.now();
|
||||
|
||||
return {
|
||||
filename: path.basename(audioFilePath),
|
||||
filepath: audioFilePath,
|
||||
fileSize: stats.size,
|
||||
transcription: response.text,
|
||||
wordCount: response.text.split(/\s+/).length,
|
||||
processingTime: endTime - startTime,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
const result = await transcribeWithMetadata('./meeting_recording.wav');
|
||||
console.log('Transcription Details:', JSON.stringify(result, null, 2));
|
||||
```
|
||||
|
||||
### Real-time Audio Processing Service
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
class ASRService {
|
||||
constructor() {
|
||||
this.zai = null;
|
||||
this.transcriptionCache = new Map();
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
generateCacheKey(audioBuffer) {
|
||||
const crypto = require('crypto');
|
||||
return crypto.createHash('md5').update(audioBuffer).digest('hex');
|
||||
}
|
||||
|
||||
async transcribe(audioFilePath, useCache = true) {
|
||||
const audioBuffer = fs.readFileSync(audioFilePath);
|
||||
const cacheKey = this.generateCacheKey(audioBuffer);
|
||||
|
||||
// Check cache
|
||||
if (useCache && this.transcriptionCache.has(cacheKey)) {
|
||||
return {
|
||||
transcription: this.transcriptionCache.get(cacheKey),
|
||||
cached: true
|
||||
};
|
||||
}
|
||||
|
||||
// Transcribe audio
|
||||
const base64Audio = audioBuffer.toString('base64');
|
||||
|
||||
const response = await this.zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
// Cache result
|
||||
if (useCache) {
|
||||
this.transcriptionCache.set(cacheKey, response.text);
|
||||
}
|
||||
|
||||
return {
|
||||
transcription: response.text,
|
||||
cached: false
|
||||
};
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.transcriptionCache.clear();
|
||||
}
|
||||
|
||||
getCacheSize() {
|
||||
return this.transcriptionCache.size;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const asrService = new ASRService();
|
||||
await asrService.initialize();
|
||||
|
||||
const result1 = await asrService.transcribe('./audio.wav');
|
||||
console.log('First call (not cached):', result1);
|
||||
|
||||
const result2 = await asrService.transcribe('./audio.wav');
|
||||
console.log('Second call (cached):', result2);
|
||||
```
|
||||
|
||||
### Directory Transcription
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function transcribeDirectory(directoryPath, outputJsonPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Get all audio files
|
||||
const files = fs.readdirSync(directoryPath);
|
||||
const audioFiles = files.filter(file =>
|
||||
/\.(wav|mp3|m4a|flac|ogg)$/i.test(file)
|
||||
);
|
||||
|
||||
const results = {
|
||||
directory: directoryPath,
|
||||
totalFiles: audioFiles.length,
|
||||
processedAt: new Date().toISOString(),
|
||||
transcriptions: []
|
||||
};
|
||||
|
||||
for (const filename of audioFiles) {
|
||||
const filePath = path.join(directoryPath, filename);
|
||||
|
||||
try {
|
||||
const audioFile = fs.readFileSync(filePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
results.transcriptions.push({
|
||||
filename: filename,
|
||||
success: true,
|
||||
text: response.text,
|
||||
wordCount: response.text.split(/\s+/).length
|
||||
});
|
||||
|
||||
console.log(`✓ Transcribed: ${filename}`);
|
||||
} catch (error) {
|
||||
results.transcriptions.push({
|
||||
filename: filename,
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
console.error(`✗ Failed: ${filename} - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Save results to JSON
|
||||
fs.writeFileSync(
|
||||
outputJsonPath,
|
||||
JSON.stringify(results, null, 2)
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const results = await transcribeDirectory(
|
||||
'./audio-recordings',
|
||||
'./transcriptions.json'
|
||||
);
|
||||
|
||||
console.log(`\nProcessed ${results.totalFiles} files`);
|
||||
console.log(`Successful: ${results.transcriptions.filter(t => t.success).length}`);
|
||||
console.log(`Failed: ${results.transcriptions.filter(t => !t.success).length}`);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Audio Format Handling
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function transcribeAnyFormat(audioFilePath) {
|
||||
// Supported formats: WAV, MP3, M4A, FLAC, OGG, etc.
|
||||
const validExtensions = ['.wav', '.mp3', '.m4a', '.flac', '.ogg'];
|
||||
const ext = audioFilePath.toLowerCase().substring(audioFilePath.lastIndexOf('.'));
|
||||
|
||||
if (!validExtensions.includes(ext)) {
|
||||
throw new Error(`Unsupported audio format: ${ext}`);
|
||||
}
|
||||
|
||||
const zai = await ZAI.create();
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
return response.text;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function safeTranscribe(audioFilePath) {
|
||||
try {
|
||||
// Validate file exists
|
||||
if (!fs.existsSync(audioFilePath)) {
|
||||
throw new Error(`File not found: ${audioFilePath}`);
|
||||
}
|
||||
|
||||
// Check file size (e.g., limit to 100MB)
|
||||
const stats = fs.statSync(audioFilePath);
|
||||
const fileSizeMB = stats.size / (1024 * 1024);
|
||||
|
||||
if (fileSizeMB > 100) {
|
||||
throw new Error(`File too large: ${fileSizeMB.toFixed(2)}MB (max 100MB)`);
|
||||
}
|
||||
|
||||
// Transcribe
|
||||
const zai = await ZAI.create();
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
if (!response.text || response.text.trim().length === 0) {
|
||||
throw new Error('Empty transcription result');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transcription: response.text,
|
||||
filePath: audioFilePath,
|
||||
fileSize: stats.size
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Transcription error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
filePath: audioFilePath
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Post-Processing Transcriptions
|
||||
|
||||
```javascript
|
||||
function cleanTranscription(text) {
|
||||
// Remove excessive whitespace
|
||||
text = text.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Capitalize first letter of sentences
|
||||
text = text.replace(/(^\w|[.!?]\s+\w)/g, match => match.toUpperCase());
|
||||
|
||||
// Remove filler words (optional)
|
||||
const fillers = ['um', 'uh', 'ah', 'like', 'you know'];
|
||||
const fillerPattern = new RegExp(`\\b(${fillers.join('|')})\\b`, 'gi');
|
||||
text = text.replace(fillerPattern, '').replace(/\s+/g, ' ');
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function transcribeAndClean(audioFilePath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const audioFile = fs.readFileSync(audioFilePath);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zai.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
return {
|
||||
raw: response.text,
|
||||
cleaned: cleanTranscription(response.text)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Meeting Transcription**: Convert recorded meetings into searchable text
|
||||
2. **Interview Processing**: Transcribe interviews for analysis and documentation
|
||||
3. **Podcast Transcription**: Create text versions of podcast episodes
|
||||
4. **Voice Notes**: Convert voice memos to text for easier reference
|
||||
5. **Call Center Analytics**: Analyze customer service calls
|
||||
6. **Accessibility**: Provide text alternatives for audio content
|
||||
7. **Voice Commands**: Enable voice-controlled applications
|
||||
8. **Language Learning**: Transcribe pronunciation practice
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express.js API Endpoint
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
const app = express();
|
||||
const upload = multer({ dest: 'uploads/' });
|
||||
|
||||
let zaiInstance;
|
||||
|
||||
async function initZAI() {
|
||||
zaiInstance = await ZAI.create();
|
||||
}
|
||||
|
||||
app.post('/api/transcribe', upload.single('audio'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: 'No audio file provided' });
|
||||
}
|
||||
|
||||
const audioFile = fs.readFileSync(req.file.path);
|
||||
const base64Audio = audioFile.toString('base64');
|
||||
|
||||
const response = await zaiInstance.audio.asr.create({
|
||||
file_base64: base64Audio
|
||||
});
|
||||
|
||||
// Clean up uploaded file
|
||||
fs.unlinkSync(req.file.path);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
transcription: response.text,
|
||||
wordCount: response.text.split(/\s+/).length
|
||||
});
|
||||
} catch (error) {
|
||||
// Clean up on error
|
||||
if (req.file && fs.existsSync(req.file.path)) {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
initZAI().then(() => {
|
||||
app.listen(3000, () => {
|
||||
console.log('ASR API running on port 3000');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "SDK must be used in backend"
|
||||
- **Solution**: Ensure z-ai-web-dev-sdk is only imported in server-side code
|
||||
|
||||
**Issue**: Empty or incorrect transcription
|
||||
- **Solution**: Verify audio quality and format. Check if audio contains clear speech
|
||||
|
||||
**Issue**: Large file processing fails
|
||||
- **Solution**: Consider splitting large audio files into smaller segments
|
||||
|
||||
**Issue**: Slow transcription speed
|
||||
- **Solution**: Implement caching for repeated transcriptions, optimize file sizes
|
||||
|
||||
**Issue**: Memory errors with large files
|
||||
- **Solution**: Process files in chunks or increase Node.js memory limit
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reuse SDK Instance**: Create once, use multiple times
|
||||
2. **Implement Caching**: Cache transcriptions for duplicate files
|
||||
3. **Batch Processing**: Process multiple files efficiently with proper queuing
|
||||
4. **Audio Optimization**: Compress audio files before processing when possible
|
||||
5. **Async Operations**: Use Promise.all for parallel processing when appropriate
|
||||
|
||||
## Audio Quality Guidelines
|
||||
|
||||
For best transcription results:
|
||||
- **Sample Rate**: 16kHz or higher
|
||||
- **Format**: WAV, MP3, or M4A recommended
|
||||
- **Noise Level**: Minimize background noise
|
||||
- **Speech Clarity**: Clear pronunciation and normal speaking pace
|
||||
- **File Size**: Under 100MB recommended for individual files
|
||||
|
||||
## Remember
|
||||
|
||||
- Always use z-ai-web-dev-sdk in backend code only
|
||||
- The SDK is already installed - import as shown in examples
|
||||
- Audio files must be converted to base64 before processing
|
||||
- Implement proper error handling for production applications
|
||||
- Consider audio quality for best transcription accuracy
|
||||
- Clean up temporary files after processing
|
||||
- Cache results for frequently transcribed files
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function main(inputFile: string) {
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
console.error(`Audio file not found: ${inputFile}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const audioBuffer = fs.readFileSync(inputFile);
|
||||
const file_base64 = audioBuffer.toString('base64');
|
||||
|
||||
const result = await zai.audio.asr.create({ file_base64 });
|
||||
|
||||
console.log('Transcription result:');
|
||||
console.log(result.text ?? JSON.stringify(result, null, 2));
|
||||
} catch (err: any) {
|
||||
console.error('ASR failed:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
main('./output.wav');
|
||||
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 z-ai-web-dev-sdk Skills
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+856
@@ -0,0 +1,856 @@
|
||||
---
|
||||
name: LLM
|
||||
description: Implement large language model (LLM) chat completions using the z-ai-web-dev-sdk. Use this skill when the user needs to build conversational AI applications, chatbots, AI assistants, or any text generation features. Supports multi-turn conversations, system prompts, and context management.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# LLM (Large Language Model) Skill
|
||||
|
||||
This skill guides the implementation of chat completions functionality using the z-ai-web-dev-sdk package, enabling powerful conversational AI and text generation capabilities.
|
||||
|
||||
## Skills Path
|
||||
|
||||
**Skill Location**: `{project_path}/skills/llm`
|
||||
|
||||
this skill is located at above path in your project.
|
||||
|
||||
**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/chat.ts` for a working example.
|
||||
|
||||
## Overview
|
||||
|
||||
The LLM skill allows you to build applications that leverage large language models for natural language understanding and generation, including chatbots, AI assistants, content generation, and more.
|
||||
|
||||
**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below.
|
||||
|
||||
## CLI Usage (For Simple Tasks)
|
||||
|
||||
For simple, one-off chat completions, you can use the z-ai CLI instead of writing code. This is ideal for quick tests, simple queries, or automation scripts.
|
||||
|
||||
### Basic Chat
|
||||
|
||||
```bash
|
||||
# Simple question
|
||||
z-ai chat --prompt "What is the capital of France?"
|
||||
|
||||
# Save response to file
|
||||
z-ai chat -p "Explain quantum computing" -o response.json
|
||||
|
||||
# Stream the response
|
||||
z-ai chat -p "Write a short poem" --stream
|
||||
```
|
||||
|
||||
### With System Prompt
|
||||
|
||||
```bash
|
||||
# Custom system prompt for specific behavior
|
||||
z-ai chat \
|
||||
--prompt "Review this code: function add(a,b) { return a+b; }" \
|
||||
--system "You are an expert code reviewer" \
|
||||
-o review.json
|
||||
```
|
||||
|
||||
### With Thinking (Chain of Thought)
|
||||
|
||||
```bash
|
||||
# Enable thinking for complex reasoning
|
||||
z-ai chat \
|
||||
--prompt "Solve this math problem: If a train travels 120km in 2 hours, what's its speed?" \
|
||||
--thinking \
|
||||
-o solution.json
|
||||
```
|
||||
|
||||
### CLI Parameters
|
||||
|
||||
- `--prompt, -p <text>`: **Required** - User message content
|
||||
- `--system, -s <text>`: Optional - System prompt for custom behavior
|
||||
- `--thinking, -t`: Optional - Enable chain-of-thought reasoning (default: disabled)
|
||||
- `--output, -o <path>`: Optional - Output file path (JSON format)
|
||||
- `--stream`: Optional - Stream the response in real-time
|
||||
|
||||
### When to Use CLI vs SDK
|
||||
|
||||
**Use CLI for:**
|
||||
- Quick one-off questions
|
||||
- Simple automation scripts
|
||||
- Testing prompts
|
||||
- Single-turn conversations
|
||||
|
||||
**Use SDK for:**
|
||||
- Multi-turn conversations with context
|
||||
- Custom conversation management
|
||||
- Integration with web applications
|
||||
- Complex chat workflows
|
||||
- Production applications
|
||||
|
||||
## Basic Chat Completions
|
||||
|
||||
### Simple Question and Answer
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function askQuestion(question) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are a helpful assistant.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: question
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
return response;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const answer = await askQuestion('What is the capital of France?');
|
||||
console.log('Answer:', answer);
|
||||
```
|
||||
|
||||
### Custom System Prompt
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function customAssistant(systemPrompt, userMessage) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
// Usage - Code reviewer
|
||||
const codeReview = await customAssistant(
|
||||
'You are an expert code reviewer. Analyze code for bugs, performance issues, and best practices.',
|
||||
'Review this function: function add(a, b) { return a + b; }'
|
||||
);
|
||||
|
||||
// Usage - Creative writer
|
||||
const story = await customAssistant(
|
||||
'You are a creative fiction writer who writes engaging short stories.',
|
||||
'Write a short story about a robot learning to paint.'
|
||||
);
|
||||
|
||||
console.log(codeReview);
|
||||
console.log(story);
|
||||
```
|
||||
|
||||
## Multi-turn Conversations
|
||||
|
||||
### Conversation History Management
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class ConversationManager {
|
||||
constructor(systemPrompt = 'You are a helpful assistant.') {
|
||||
this.messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
}
|
||||
];
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async sendMessage(userMessage) {
|
||||
// Add user message to history
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
});
|
||||
|
||||
// Get completion
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: this.messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const assistantResponse = completion.choices[0]?.message?.content;
|
||||
|
||||
// Add assistant response to history
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: assistantResponse
|
||||
});
|
||||
|
||||
return assistantResponse;
|
||||
}
|
||||
|
||||
getHistory() {
|
||||
return this.messages;
|
||||
}
|
||||
|
||||
clearHistory(systemPrompt = 'You are a helpful assistant.') {
|
||||
this.messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getMessageCount() {
|
||||
// Subtract 1 for system message
|
||||
return this.messages.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const conversation = new ConversationManager();
|
||||
await conversation.initialize();
|
||||
|
||||
const response1 = await conversation.sendMessage('Hi, my name is John.');
|
||||
console.log('AI:', response1);
|
||||
|
||||
const response2 = await conversation.sendMessage('What is my name?');
|
||||
console.log('AI:', response2); // Should remember the name is John
|
||||
|
||||
console.log('Total messages:', conversation.getMessageCount());
|
||||
```
|
||||
|
||||
### Context-Aware Conversations
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class ContextualChat {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async startConversation(role, context) {
|
||||
// Set up system prompt with context
|
||||
const systemPrompt = `You are ${role}. Context: ${context}`;
|
||||
|
||||
this.messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async chat(userMessage) {
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
});
|
||||
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: this.messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: response
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage - Customer support scenario
|
||||
const support = new ContextualChat();
|
||||
await support.initialize();
|
||||
|
||||
await support.startConversation(
|
||||
'a customer support agent for TechCorp',
|
||||
'The user has ordered product #12345 which is delayed due to shipping issues.'
|
||||
);
|
||||
|
||||
const reply1 = await support.chat('Where is my order?');
|
||||
console.log('Support:', reply1);
|
||||
|
||||
const reply2 = await support.chat('Can I get a refund?');
|
||||
console.log('Support:', reply2);
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Content Generation
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class ContentGenerator {
|
||||
constructor() {
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async generateBlogPost(topic, tone = 'professional') {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are a professional content writer. Write in a ${tone} tone.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Write a blog post about: ${topic}. Include an introduction, main points, and conclusion.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
async generateProductDescription(productName, features) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are an expert at writing compelling product descriptions for e-commerce.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Write a product description for "${productName}". Key features: ${features.join(', ')}.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
async generateEmailResponse(originalEmail, intent) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are a professional email writer. Write clear, concise, and polite emails.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Original email: "${originalEmail}"\n\nWrite a ${intent} response.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const generator = new ContentGenerator();
|
||||
await generator.initialize();
|
||||
|
||||
const blogPost = await generator.generateBlogPost(
|
||||
'The Future of Artificial Intelligence',
|
||||
'informative'
|
||||
);
|
||||
console.log('Blog Post:', blogPost);
|
||||
|
||||
const productDesc = await generator.generateProductDescription(
|
||||
'Smart Watch Pro',
|
||||
['Heart rate monitoring', 'GPS tracking', 'Waterproof', '7-day battery life']
|
||||
);
|
||||
console.log('Product Description:', productDesc);
|
||||
```
|
||||
|
||||
### Data Analysis and Summarization
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function analyzeData(data, analysisType) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const prompts = {
|
||||
summarize: 'You are a data analyst. Summarize the key insights from the data.',
|
||||
trend: 'You are a data analyst. Identify trends and patterns in the data.',
|
||||
recommendation: 'You are a business analyst. Provide actionable recommendations based on the data.'
|
||||
};
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: prompts[analysisType] || prompts.summarize
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Analyze this data:\n\n${JSON.stringify(data, null, 2)}`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const salesData = {
|
||||
Q1: { revenue: 100000, customers: 250 },
|
||||
Q2: { revenue: 120000, customers: 280 },
|
||||
Q3: { revenue: 150000, customers: 320 },
|
||||
Q4: { revenue: 180000, customers: 380 }
|
||||
};
|
||||
|
||||
const summary = await analyzeData(salesData, 'summarize');
|
||||
const trends = await analyzeData(salesData, 'trend');
|
||||
const recommendations = await analyzeData(salesData, 'recommendation');
|
||||
|
||||
console.log('Summary:', summary);
|
||||
console.log('Trends:', trends);
|
||||
console.log('Recommendations:', recommendations);
|
||||
```
|
||||
|
||||
### Code Generation and Debugging
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class CodeAssistant {
|
||||
constructor() {
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async generateCode(description, language) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are an expert ${language} programmer. Write clean, efficient, and well-commented code.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Write ${language} code to: ${description}`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
async debugCode(code, issue) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are an expert debugger. Identify bugs and suggest fixes.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Code:\n${code}\n\nIssue: ${issue}\n\nFind the bug and suggest a fix.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
async explainCode(code) {
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: 'You are a programming teacher. Explain code clearly and simply.'
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Explain what this code does:\n\n${code}`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const codeAssist = new CodeAssistant();
|
||||
await codeAssist.initialize();
|
||||
|
||||
const newCode = await codeAssist.generateCode(
|
||||
'Create a function that sorts an array of objects by a specific property',
|
||||
'JavaScript'
|
||||
);
|
||||
console.log('Generated Code:', newCode);
|
||||
|
||||
const bugFix = await codeAssist.debugCode(
|
||||
'function add(a, b) { return a - b; }',
|
||||
'This function should add numbers but returns wrong results'
|
||||
);
|
||||
console.log('Debug Suggestion:', bugFix);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Prompt Engineering
|
||||
|
||||
```javascript
|
||||
// Bad: Vague prompt
|
||||
const bad = await askQuestion('Tell me about AI');
|
||||
|
||||
// Good: Specific and structured prompt
|
||||
async function askWithContext(topic, format, audience) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are an expert educator. Explain topics clearly for ${audience}.`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: `Explain ${topic} in ${format} format. Include practical examples.`
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return completion.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
const good = await askWithContext('artificial intelligence', 'bullet points', 'beginners');
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function safeCompletion(messages, retries = 3) {
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
|
||||
if (!response || response.trim().length === 0) {
|
||||
throw new Error('Empty response from AI');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: response,
|
||||
attempts: attempt
|
||||
};
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.error(`Attempt ${attempt} failed:`, error.message);
|
||||
|
||||
if (attempt < retries) {
|
||||
// Wait before retry (exponential backoff)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: lastError.message,
|
||||
attempts: retries
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Context Management
|
||||
|
||||
```javascript
|
||||
class ManagedConversation {
|
||||
constructor(maxMessages = 20) {
|
||||
this.maxMessages = maxMessages;
|
||||
this.systemPrompt = '';
|
||||
this.messages = [];
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize(systemPrompt) {
|
||||
this.zai = await ZAI.create();
|
||||
this.systemPrompt = systemPrompt;
|
||||
this.messages = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async chat(userMessage) {
|
||||
// Add user message
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
});
|
||||
|
||||
// Trim old messages if exceeding limit (keep system prompt)
|
||||
if (this.messages.length > this.maxMessages) {
|
||||
this.messages = [
|
||||
this.messages[0], // Keep system prompt
|
||||
...this.messages.slice(-(this.maxMessages - 1))
|
||||
];
|
||||
}
|
||||
|
||||
const completion = await this.zai.chat.completions.create({
|
||||
messages: this.messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: response
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
getTokenEstimate() {
|
||||
// Rough estimate: ~4 characters per token
|
||||
const totalChars = this.messages
|
||||
.map(m => m.content.length)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
return Math.ceil(totalChars / 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Response Processing
|
||||
|
||||
```javascript
|
||||
async function getStructuredResponse(query, format = 'json') {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const formatInstructions = {
|
||||
json: 'Respond with valid JSON only. No additional text.',
|
||||
list: 'Respond with a numbered list.',
|
||||
markdown: 'Respond in Markdown format.'
|
||||
};
|
||||
|
||||
const completion = await zai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: `You are a helpful assistant. ${formatInstructions[format]}`
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: query
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const response = completion.choices[0]?.message?.content;
|
||||
|
||||
// Parse JSON if requested
|
||||
if (format === 'json') {
|
||||
try {
|
||||
return JSON.parse(response);
|
||||
} catch (e) {
|
||||
console.error('Failed to parse JSON response');
|
||||
return { raw: response };
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const jsonData = await getStructuredResponse(
|
||||
'List three programming languages with their primary use cases',
|
||||
'json'
|
||||
);
|
||||
console.log(jsonData);
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Chatbots & Virtual Assistants**: Build conversational interfaces for customer support
|
||||
2. **Content Generation**: Create articles, product descriptions, marketing copy
|
||||
3. **Code Assistance**: Generate, explain, and debug code
|
||||
4. **Data Analysis**: Analyze and summarize complex data sets
|
||||
5. **Language Translation**: Translate text between languages
|
||||
6. **Educational Tools**: Create tutoring and learning applications
|
||||
7. **Email Automation**: Generate professional email responses
|
||||
8. **Creative Writing**: Story generation, poetry, and creative content
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express.js Chatbot API
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Store conversations in memory (use database in production)
|
||||
const conversations = new Map();
|
||||
|
||||
let zaiInstance;
|
||||
|
||||
async function initZAI() {
|
||||
zaiInstance = await ZAI.create();
|
||||
}
|
||||
|
||||
app.post('/api/chat', async (req, res) => {
|
||||
try {
|
||||
const { sessionId, message, systemPrompt } = req.body;
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
}
|
||||
|
||||
// Get or create conversation history
|
||||
let history = conversations.get(sessionId) || [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: systemPrompt || 'You are a helpful assistant.'
|
||||
}
|
||||
];
|
||||
|
||||
// Add user message
|
||||
history.push({
|
||||
role: 'user',
|
||||
content: message
|
||||
});
|
||||
|
||||
// Get completion
|
||||
const completion = await zaiInstance.chat.completions.create({
|
||||
messages: history,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const aiResponse = completion.choices[0]?.message?.content;
|
||||
|
||||
// Add AI response to history
|
||||
history.push({
|
||||
role: 'assistant',
|
||||
content: aiResponse
|
||||
});
|
||||
|
||||
// Save updated history
|
||||
conversations.set(sessionId, history);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
response: aiResponse,
|
||||
messageCount: history.length - 1
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/chat/:sessionId', (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
conversations.delete(sessionId);
|
||||
res.json({ success: true, message: 'Conversation cleared' });
|
||||
});
|
||||
|
||||
initZAI().then(() => {
|
||||
app.listen(3000, () => {
|
||||
console.log('Chatbot API running on port 3000');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "SDK must be used in backend"
|
||||
- **Solution**: Ensure z-ai-web-dev-sdk is only imported and used in server-side code
|
||||
|
||||
**Issue**: Empty or incomplete responses
|
||||
- **Solution**: Check that completion.choices[0]?.message?.content exists and is not empty
|
||||
|
||||
**Issue**: Conversation context getting too long
|
||||
- **Solution**: Implement message trimming to keep only recent messages
|
||||
|
||||
**Issue**: Inconsistent responses
|
||||
- **Solution**: Use more specific system prompts and provide clear instructions
|
||||
|
||||
**Issue**: Rate limiting errors
|
||||
- **Solution**: Implement retry logic with exponential backoff
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reuse SDK Instance**: Create ZAI instance once and reuse across requests
|
||||
2. **Manage Context Length**: Trim old messages to avoid token limits
|
||||
3. **Implement Caching**: Cache responses for common queries
|
||||
4. **Use Specific Prompts**: Clear prompts lead to faster, better responses
|
||||
5. **Handle Errors Gracefully**: Implement retry logic and fallback responses
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Input Validation**: Always validate and sanitize user input
|
||||
2. **Rate Limiting**: Implement rate limits to prevent abuse
|
||||
3. **API Key Protection**: Never expose SDK credentials in client-side code
|
||||
4. **Content Filtering**: Filter sensitive or inappropriate content
|
||||
5. **Session Management**: Implement proper session handling and cleanup
|
||||
|
||||
## Remember
|
||||
|
||||
- Always use z-ai-web-dev-sdk in backend code only
|
||||
- The SDK is already installed - import as shown in examples
|
||||
- Use the 'assistant' role for system prompts
|
||||
- Set thinking to { type: 'disabled' } for standard completions
|
||||
- Implement proper error handling and retries for production
|
||||
- Manage conversation history to avoid token limits
|
||||
- Clear and specific prompts lead to better results
|
||||
- Check `scripts/chat.ts` for a quick start example
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
import ZAI, { ChatMessage } from "z-ai-web-dev-sdk";
|
||||
|
||||
async function main(prompt: string) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const messages: ChatMessage[] = [
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Hi, I'm a helpful assistant."
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await zai.chat.completions.create({
|
||||
messages,
|
||||
stream: false,
|
||||
thinking: { type: "disabled" },
|
||||
});
|
||||
|
||||
const reply = response.choices?.[0]?.message?.content;
|
||||
console.log("Chat reply:");
|
||||
console.log(reply ?? JSON.stringify(response, null, 2));
|
||||
} catch (err: any) {
|
||||
console.error("Chat failed:", err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
main('What is the capital of France?');
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 z-ai-web-dev-sdk Skills
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+735
@@ -0,0 +1,735 @@
|
||||
---
|
||||
name: TTS
|
||||
description: Implement text-to-speech (TTS) capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to convert text into natural-sounding speech, create audio content, build voice-enabled applications, or generate spoken audio files. Supports multiple voices, adjustable speed, and various audio formats.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# TTS (Text to Speech) Skill
|
||||
|
||||
This skill guides the implementation of text-to-speech (TTS) functionality using the z-ai-web-dev-sdk package, enabling conversion of text into natural-sounding speech audio.
|
||||
|
||||
## Skills Path
|
||||
|
||||
**Skill Location**: `{project_path}/skills/TTS`
|
||||
|
||||
This skill is located at the above path in your project.
|
||||
|
||||
**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/tts.ts` for a working example.
|
||||
|
||||
## Overview
|
||||
|
||||
Text-to-Speech allows you to build applications that generate spoken audio from text input, supporting various voices, speeds, and output formats for diverse use cases.
|
||||
|
||||
**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code.
|
||||
|
||||
## API Limitations and Constraints
|
||||
|
||||
Before implementing TTS functionality, be aware of these important limitations:
|
||||
|
||||
### Input Text Constraints
|
||||
- **Maximum length**: 1024 characters per request
|
||||
- Text exceeding this limit must be split into smaller chunks
|
||||
|
||||
### Audio Parameters
|
||||
- **Speed range**: 0.5 to 2.0
|
||||
- 0.5 = half speed (slower)
|
||||
- 1.0 = normal speed (default)
|
||||
- 2.0 = double speed (faster)
|
||||
- **Volume range**: Greater than 0, up to 10
|
||||
- Default: 1.0
|
||||
- Values must be greater than 0 (exclusive) and up to 10 (inclusive)
|
||||
|
||||
### Format and Streaming
|
||||
- **Streaming limitation**: When `stream: true` is enabled, only `pcm` format is supported
|
||||
- **Non-streaming**: Supports `wav`, `pcm`, and `mp3` formats
|
||||
- **Sample rate**: 24000 Hz (recommended)
|
||||
|
||||
### Best Practice for Long Text
|
||||
```javascript
|
||||
function splitTextIntoChunks(text, maxLength = 1000) {
|
||||
const chunks = [];
|
||||
const sentences = text.match(/[^.!?]+[.!?]+/g) || [text];
|
||||
|
||||
let currentChunk = '';
|
||||
for (const sentence of sentences) {
|
||||
if ((currentChunk + sentence).length <= maxLength) {
|
||||
currentChunk += sentence;
|
||||
} else {
|
||||
if (currentChunk) chunks.push(currentChunk.trim());
|
||||
currentChunk = sentence;
|
||||
}
|
||||
}
|
||||
if (currentChunk) chunks.push(currentChunk.trim());
|
||||
|
||||
return chunks;
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below.
|
||||
|
||||
## CLI Usage (For Simple Tasks)
|
||||
|
||||
For simple text-to-speech conversions, you can use the z-ai CLI instead of writing code. This is ideal for quick audio generation, testing voices, or simple automation.
|
||||
|
||||
### Basic TTS
|
||||
|
||||
```bash
|
||||
# Convert text to speech (default WAV format)
|
||||
z-ai tts --input "Hello, world" --output ./hello.wav
|
||||
|
||||
# Using short options
|
||||
z-ai tts -i "Hello, world" -o ./hello.wav
|
||||
```
|
||||
|
||||
### Different Voices and Speed
|
||||
|
||||
```bash
|
||||
# Use specific voice
|
||||
z-ai tts -i "Welcome to our service" -o ./welcome.wav --voice tongtong
|
||||
|
||||
# Adjust speech speed (0.5-2.0)
|
||||
z-ai tts -i "This is faster speech" -o ./fast.wav --speed 1.5
|
||||
|
||||
# Slower speech
|
||||
z-ai tts -i "This is slower speech" -o ./slow.wav --speed 0.8
|
||||
```
|
||||
|
||||
### Different Output Formats
|
||||
|
||||
```bash
|
||||
# MP3 format
|
||||
z-ai tts -i "Hello World" -o ./hello.mp3 --format mp3
|
||||
|
||||
# WAV format (default)
|
||||
z-ai tts -i "Hello World" -o ./hello.wav --format wav
|
||||
|
||||
# PCM format
|
||||
z-ai tts -i "Hello World" -o ./hello.pcm --format pcm
|
||||
```
|
||||
|
||||
### Streaming Output
|
||||
|
||||
```bash
|
||||
# Stream audio generation
|
||||
z-ai tts -i "This is a longer text that will be streamed" -o ./stream.wav --stream
|
||||
```
|
||||
|
||||
### CLI Parameters
|
||||
|
||||
- `--input, -i <text>`: **Required** - Text to convert to speech (max 1024 characters)
|
||||
- `--output, -o <path>`: **Required** - Output audio file path
|
||||
- `--voice, -v <voice>`: Optional - Voice type (default: tongtong)
|
||||
- `--speed, -s <number>`: Optional - Speech speed, 0.5-2.0 (default: 1.0)
|
||||
- `--format, -f <format>`: Optional - Output format: wav, mp3, pcm (default: wav)
|
||||
- `--stream`: Optional - Enable streaming output (only supports pcm format)
|
||||
|
||||
### When to Use CLI vs SDK
|
||||
|
||||
**Use CLI for:**
|
||||
- Quick text-to-speech conversions
|
||||
- Testing different voices and speeds
|
||||
- Simple batch audio generation
|
||||
- Command-line automation scripts
|
||||
|
||||
**Use SDK for:**
|
||||
- Dynamic audio generation in applications
|
||||
- Integration with web services
|
||||
- Custom audio processing pipelines
|
||||
- Production applications with complex requirements
|
||||
|
||||
## Basic TTS Implementation
|
||||
|
||||
### Simple Text to Speech
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function textToSpeech(text, outputPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
console.log(`Audio saved to ${outputPath}`);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Usage
|
||||
await textToSpeech('Hello, world!', './output.wav');
|
||||
```
|
||||
|
||||
### Multiple Voice Options
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function generateWithVoice(text, voice, outputPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: voice, // Available voices: tongtong, chuichui, xiaochen, jam, kazi, douji, luodo
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Usage
|
||||
await generateWithVoice('Welcome to our service', 'tongtong', './welcome.wav');
|
||||
```
|
||||
|
||||
### Adjustable Speed
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function generateWithSpeed(text, speed, outputPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Speed range: 0.5 to 2.0 (API constraint)
|
||||
// 0.5 = half speed (slower)
|
||||
// 1.0 = normal speed (default)
|
||||
// 2.0 = double speed (faster)
|
||||
// Values outside this range will cause API errors
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: speed,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Usage - slower narration
|
||||
await generateWithSpeed('This is an important announcement', 0.8, './slow.wav');
|
||||
|
||||
// Usage - faster narration
|
||||
await generateWithSpeed('Quick update', 1.3, './fast.wav');
|
||||
```
|
||||
|
||||
### Adjustable Volume
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function generateWithVolume(text, volume, outputPath) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Volume range: greater than 0, up to 10 (API constraint)
|
||||
// Values must be > 0 (exclusive) and <= 10 (inclusive)
|
||||
// Default: 1.0 (normal volume)
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
volume: volume, // Optional parameter
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
// Usage - louder audio
|
||||
await generateWithVolume('This is an announcement', 5.0, './loud.wav');
|
||||
|
||||
// Usage - quieter audio
|
||||
await generateWithVolume('Whispered message', 0.5, './quiet.wav');
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Batch Processing
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
async function batchTextToSpeech(textArray, outputDir) {
|
||||
const zai = await ZAI.create();
|
||||
const results = [];
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
for (let i = 0; i < textArray.length; i++) {
|
||||
try {
|
||||
const text = textArray[i];
|
||||
const outputPath = path.join(outputDir, `audio_${i + 1}.wav`);
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
results.push({
|
||||
success: true,
|
||||
text,
|
||||
path: outputPath
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
success: false,
|
||||
text: textArray[i],
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const texts = [
|
||||
'Welcome to chapter one',
|
||||
'Welcome to chapter two',
|
||||
'Welcome to chapter three'
|
||||
];
|
||||
|
||||
const results = await batchTextToSpeech(texts, './audio-output');
|
||||
console.log('Generated:', results.length, 'audio files');
|
||||
```
|
||||
|
||||
### Dynamic Content Generation
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
class TTSGenerator {
|
||||
constructor() {
|
||||
this.zai = null;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async generateAudio(text, options = {}) {
|
||||
const {
|
||||
voice = 'tongtong',
|
||||
speed = 1.0,
|
||||
format = 'wav'
|
||||
} = options;
|
||||
|
||||
const response = await this.zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: voice,
|
||||
speed: speed,
|
||||
response_format: format,
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
return Buffer.from(new Uint8Array(arrayBuffer));
|
||||
}
|
||||
|
||||
async saveAudio(text, outputPath, options = {}) {
|
||||
const buffer = await this.generateAudio(text, options);
|
||||
if (buffer) {
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
return outputPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const generator = new TTSGenerator();
|
||||
await generator.initialize();
|
||||
|
||||
await generator.saveAudio(
|
||||
'Hello, this is a test',
|
||||
'./output.wav',
|
||||
{ speed: 1.2 }
|
||||
);
|
||||
```
|
||||
|
||||
### Next.js API Route Example
|
||||
|
||||
```javascript
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { text, voice = 'tongtong', speed = 1.0 } = await req.json();
|
||||
|
||||
// Import ZAI SDK
|
||||
const ZAI = (await import('z-ai-web-dev-sdk')).default;
|
||||
|
||||
// Create SDK instance
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Generate TTS audio
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text.trim(),
|
||||
voice: voice,
|
||||
speed: speed,
|
||||
response_format: 'wav',
|
||||
stream: false,
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
// Return audio as response
|
||||
return new NextResponse(buffer, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'audio/wav',
|
||||
'Content-Length': buffer.length.toString(),
|
||||
'Cache-Control': 'no-cache',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('TTS API Error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : '生成语音失败,请稍后重试',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Text Preparation
|
||||
```javascript
|
||||
function prepareTextForTTS(text) {
|
||||
// Remove excessive whitespace
|
||||
text = text.replace(/\s+/g, ' ').trim();
|
||||
|
||||
// Expand common abbreviations for better pronunciation
|
||||
const abbreviations = {
|
||||
'Dr.': 'Doctor',
|
||||
'Mr.': 'Mister',
|
||||
'Mrs.': 'Misses',
|
||||
'etc.': 'et cetera'
|
||||
};
|
||||
|
||||
for (const [abbr, full] of Object.entries(abbreviations)) {
|
||||
text = text.replace(new RegExp(abbr, 'g'), full);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function safeTTS(text, outputPath) {
|
||||
try {
|
||||
// Validate input
|
||||
if (!text || text.trim().length === 0) {
|
||||
throw new Error('Text input cannot be empty');
|
||||
}
|
||||
|
||||
if (text.length > 1024) {
|
||||
throw new Error('Text input exceeds maximum length of 1024 characters');
|
||||
}
|
||||
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: outputPath,
|
||||
size: buffer.length
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('TTS Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. SDK Instance Reuse
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
// Create a singleton instance
|
||||
let zaiInstance = null;
|
||||
|
||||
async function getZAIInstance() {
|
||||
if (!zaiInstance) {
|
||||
zaiInstance = await ZAI.create();
|
||||
}
|
||||
return zaiInstance;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const zai = await getZAIInstance();
|
||||
const response = await zai.audio.tts.create({ ... });
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Audiobooks & Podcasts**: Convert written content to audio format
|
||||
2. **E-learning**: Create narration for educational content
|
||||
3. **Accessibility**: Provide audio versions of text content
|
||||
4. **Voice Assistants**: Generate dynamic responses
|
||||
5. **Announcements**: Create automated audio notifications
|
||||
6. **IVR Systems**: Generate phone system prompts
|
||||
7. **Content Localization**: Create audio in different languages
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express.js API Endpoint
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
let zaiInstance;
|
||||
const outputDir = './audio-output';
|
||||
|
||||
async function initZAI() {
|
||||
zaiInstance = await ZAI.create();
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
app.post('/api/tts', async (req, res) => {
|
||||
try {
|
||||
const { text, voice = 'tongtong', speed = 1.0 } = req.body;
|
||||
|
||||
if (!text) {
|
||||
return res.status(400).json({ error: 'Text is required' });
|
||||
}
|
||||
|
||||
const filename = `tts_${Date.now()}.wav`;
|
||||
const outputPath = path.join(outputDir, filename);
|
||||
|
||||
const response = await zaiInstance.audio.tts.create({
|
||||
input: text,
|
||||
voice: voice,
|
||||
speed: speed,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
|
||||
// Get array buffer from Response object
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
fs.writeFileSync(outputPath, buffer);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
audioUrl: `/audio/${filename}`,
|
||||
size: buffer.length
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/audio', express.static('audio-output'));
|
||||
|
||||
initZAI().then(() => {
|
||||
app.listen(3000, () => {
|
||||
console.log('TTS API running on port 3000');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "Input text exceeds maximum length"
|
||||
- **Solution**: Text input is limited to 1024 characters. Split longer text into chunks using the `splitTextIntoChunks` function shown in the API Limitations section
|
||||
|
||||
**Issue**: "Invalid speed parameter" or unexpected speed behavior
|
||||
- **Solution**: Speed must be between 0.5 and 2.0. Check your speed value is within this range
|
||||
|
||||
**Issue**: "Invalid volume parameter"
|
||||
- **Solution**: Volume must be greater than 0 and up to 10. Ensure volume value is in range (0, 10]
|
||||
|
||||
**Issue**: "Stream format not supported" with WAV/MP3
|
||||
- **Solution**: Streaming mode only supports PCM format. Either use `response_format: 'pcm'` with streaming, or disable streaming (`stream: false`) for WAV/MP3 output
|
||||
|
||||
**Issue**: "SDK must be used in backend"
|
||||
- **Solution**: Ensure z-ai-web-dev-sdk is only imported in server-side code
|
||||
|
||||
**Issue**: "TypeError: response.audio is undefined"
|
||||
- **Solution**: The SDK returns a standard Response object, use `await response.arrayBuffer()` instead of accessing `response.audio`
|
||||
|
||||
**Issue**: Generated audio file is empty or corrupted
|
||||
- **Solution**: Ensure you're calling `await response.arrayBuffer()` and properly converting to Buffer: `Buffer.from(new Uint8Array(arrayBuffer))`
|
||||
|
||||
**Issue**: Audio sounds unnatural
|
||||
- **Solution**: Prepare text properly (remove special characters, expand abbreviations)
|
||||
|
||||
**Issue**: Long processing times
|
||||
- **Solution**: Break long text into smaller chunks and process in parallel
|
||||
|
||||
**Issue**: Next.js caching old API route
|
||||
- **Solution**: Create a new API route endpoint or restart the dev server
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reuse SDK Instance**: Create ZAI instance once and reuse
|
||||
2. **Implement Caching**: Cache generated audio for repeated text
|
||||
3. **Batch Processing**: Process multiple texts efficiently
|
||||
4. **Optimize Text**: Remove unnecessary content before generation
|
||||
5. **Async Processing**: Use queues for handling multiple requests
|
||||
|
||||
## Important Notes
|
||||
|
||||
### API Constraints
|
||||
|
||||
**Input Text Length**: Maximum 1024 characters per request. For longer text:
|
||||
```javascript
|
||||
// Split long text into chunks
|
||||
const longText = "..."; // Your long text here
|
||||
const chunks = splitTextIntoChunks(longText, 1000);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const response = await zai.audio.tts.create({
|
||||
input: chunk,
|
||||
voice: 'tongtong',
|
||||
speed: 1.0,
|
||||
response_format: 'wav',
|
||||
stream: false
|
||||
});
|
||||
// Process each chunk...
|
||||
}
|
||||
```
|
||||
|
||||
**Streaming Format Limitation**: When using `stream: true`, only `pcm` format is supported. For `wav` or `mp3` output, use `stream: false`.
|
||||
|
||||
**Sample Rate**: Audio is generated at 24000 Hz sample rate (recommended setting for playback).
|
||||
|
||||
### Response Object Format
|
||||
|
||||
The `zai.audio.tts.create()` method returns a standard **Response** object (not a custom object with an `audio` property). Always use:
|
||||
|
||||
```javascript
|
||||
// ✅ CORRECT
|
||||
const response = await zai.audio.tts.create({ ... });
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
|
||||
// ❌ WRONG - This will not work
|
||||
const response = await zai.audio.tts.create({ ... });
|
||||
const buffer = Buffer.from(response.audio); // response.audio is undefined
|
||||
```
|
||||
|
||||
### Available Voices
|
||||
|
||||
- `tongtong` - 温暖亲切
|
||||
- `chuichui` - 活泼可爱
|
||||
- `xiaochen` - 沉稳专业
|
||||
- `jam` - 英音绅士
|
||||
- `kazi` - 清晰标准
|
||||
- `douji` - 自然流畅
|
||||
- `luodo` - 富有感染力
|
||||
|
||||
### Speed Range
|
||||
|
||||
- Minimum: `0.5` (half speed)
|
||||
- Default: `1.0` (normal speed)
|
||||
- Maximum: `2.0` (double speed)
|
||||
|
||||
**Important**: Speed values outside the range [0.5, 2.0] will result in API errors.
|
||||
|
||||
### Volume Range
|
||||
|
||||
- Minimum: Greater than `0` (exclusive)
|
||||
- Default: `1.0` (normal volume)
|
||||
- Maximum: `10` (inclusive)
|
||||
|
||||
**Note**: Volume parameter is optional. When not specified, defaults to 1.0.
|
||||
|
||||
## Remember
|
||||
|
||||
- Always use z-ai-web-dev-sdk in backend code only
|
||||
- **Input text is limited to 1024 characters maximum** - split longer text into chunks
|
||||
- **Speed must be between 0.5 and 2.0** - values outside this range will cause errors
|
||||
- **Volume must be greater than 0 and up to 10** - optional parameter with default 1.0
|
||||
- **Streaming only supports PCM format** - use non-streaming for WAV or MP3 output
|
||||
- The SDK returns a standard Response object - use `await response.arrayBuffer()`
|
||||
- Convert ArrayBuffer to Buffer using `Buffer.from(new Uint8Array(arrayBuffer))`
|
||||
- Handle audio buffers properly when saving to files
|
||||
- Implement error handling for production applications
|
||||
- Consider caching for frequently generated content
|
||||
- Clean up old audio files periodically to manage storage
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
import ZAI from "z-ai-web-dev-sdk";
|
||||
import fs from "fs";
|
||||
|
||||
async function main(text: string, outFile: string) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.audio.tts.create({
|
||||
input: text,
|
||||
voice: "tongtong",
|
||||
speed: 1.0,
|
||||
response_format: "wav",
|
||||
stream: false,
|
||||
});
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(new Uint8Array(arrayBuffer));
|
||||
fs.writeFileSync(outFile, buffer);
|
||||
console.log(`TTS audio saved to ${outFile}`);
|
||||
} catch (err: any) {
|
||||
console.error("TTS failed:", err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
main("Hello, world!", "./output.wav");
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 z-ai-web-dev-sdk Skills
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Executable
+588
@@ -0,0 +1,588 @@
|
||||
---
|
||||
name: VLM
|
||||
description: Implement vision-based AI chat capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to analyze images, describe visual content, or create applications that combine image understanding with conversational AI. Supports image URLs and base64 encoded images for multimodal interactions.
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# VLM(Vision Chat) Skill
|
||||
|
||||
This skill guides the implementation of vision chat functionality using the z-ai-web-dev-sdk package, enabling AI models to understand and respond to images combined with text prompts.
|
||||
|
||||
## Skills Path
|
||||
|
||||
**Skill Location**: `{project_path}/skills/VLM`
|
||||
|
||||
this skill is located at above path in your project.
|
||||
|
||||
**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/vlm.ts` for a working example.
|
||||
|
||||
## Overview
|
||||
|
||||
Vision Chat allows you to build applications that can analyze images, extract information from visual content, and answer questions about images through natural language conversation.
|
||||
|
||||
**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below.
|
||||
|
||||
## CLI Usage (For Simple Tasks)
|
||||
|
||||
For simple image analysis tasks, you can use the z-ai CLI instead of writing code. This is ideal for quick image descriptions, testing vision capabilities, or simple automation.
|
||||
|
||||
### Basic Image Analysis
|
||||
|
||||
```bash
|
||||
# Describe an image from URL
|
||||
z-ai vision --prompt "What's in this image?" --image "https://example.com/photo.jpg"
|
||||
|
||||
# Using short options
|
||||
z-ai vision -p "Describe this image" -i "https://example.com/image.png"
|
||||
```
|
||||
|
||||
### Analyze Local Images
|
||||
|
||||
```bash
|
||||
# Analyze a local image file
|
||||
z-ai vision -p "What objects are in this photo?" -i "./photo.jpg"
|
||||
|
||||
# Save response to file
|
||||
z-ai vision -p "Describe the scene" -i "./landscape.png" -o description.json
|
||||
```
|
||||
|
||||
### Multiple Images
|
||||
|
||||
```bash
|
||||
# Analyze multiple images at once
|
||||
z-ai vision \
|
||||
-p "Compare these two images" \
|
||||
-i "./photo1.jpg" \
|
||||
-i "./photo2.jpg" \
|
||||
-o comparison.json
|
||||
|
||||
# Multiple images with detailed analysis
|
||||
z-ai vision \
|
||||
--prompt "What are the differences between these images?" \
|
||||
--image "https://example.com/before.jpg" \
|
||||
--image "https://example.com/after.jpg"
|
||||
```
|
||||
|
||||
### With Thinking (Chain of Thought)
|
||||
|
||||
```bash
|
||||
# Enable thinking for complex visual reasoning
|
||||
z-ai vision \
|
||||
-p "Count the number of people in this image and describe their activities" \
|
||||
-i "./crowd.jpg" \
|
||||
--thinking \
|
||||
-o analysis.json
|
||||
```
|
||||
|
||||
### Streaming Output
|
||||
|
||||
```bash
|
||||
# Stream the vision analysis
|
||||
z-ai vision -p "Describe this image in detail" -i "./photo.jpg" --stream
|
||||
```
|
||||
|
||||
### CLI Parameters
|
||||
|
||||
- `--prompt, -p <text>`: **Required** - Question or instruction about the image(s)
|
||||
- `--image, -i <URL or path>`: Optional - Image URL or local file path (can be used multiple times)
|
||||
- `--thinking, -t`: Optional - Enable chain-of-thought reasoning (default: disabled)
|
||||
- `--output, -o <path>`: Optional - Output file path (JSON format)
|
||||
- `--stream`: Optional - Stream the response in real-time
|
||||
|
||||
### Supported Image Formats
|
||||
|
||||
- PNG (.png)
|
||||
- JPEG (.jpg, .jpeg)
|
||||
- GIF (.gif)
|
||||
- WebP (.webp)
|
||||
- BMP (.bmp)
|
||||
|
||||
### When to Use CLI vs SDK
|
||||
|
||||
**Use CLI for:**
|
||||
- Quick image analysis
|
||||
- Testing vision model capabilities
|
||||
- One-off image descriptions
|
||||
- Simple automation scripts
|
||||
|
||||
**Use SDK for:**
|
||||
- Multi-turn conversations with images
|
||||
- Dynamic image analysis in applications
|
||||
- Batch processing with custom logic
|
||||
- Production applications with complex workflows
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
For better performance and reliability, use base64 encoding to pass images to the model instead of image URLs.
|
||||
|
||||
## Supported Content Types
|
||||
|
||||
The Vision Chat API supports three types of media content:
|
||||
|
||||
### 1. **image_url** - For Image Files
|
||||
Use this type for static images (PNG, JPEG, GIF, WebP, etc.)
|
||||
```typescript
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', image_url: { url: imageUrl } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **video_url** - For Video Files
|
||||
Use this type for video content (MP4, AVI, MOV, etc.)
|
||||
```typescript
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'video_url', video_url: { url: videoUrl } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **file_url** - For Document Files
|
||||
Use this type for document files (PDF, DOCX, TXT, etc.)
|
||||
```typescript
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'file_url', file_url: { url: fileUrl } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: You can combine multiple content types in a single message. For example, you can include both text and multiple images, or text with both an image and a document.
|
||||
|
||||
## Basic Vision Chat Implementation
|
||||
|
||||
### Single Image Analysis
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function analyzeImage(imageUrl, question) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: question
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: imageUrl
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const result = await analyzeImage(
|
||||
'https://example.com/product.jpg',
|
||||
'Describe this product in detail'
|
||||
);
|
||||
console.log('Analysis:', result);
|
||||
```
|
||||
|
||||
### Multiple Images Analysis
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function compareImages(imageUrls, question) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: question
|
||||
},
|
||||
...imageUrls.map(url => ({
|
||||
type: 'image_url',
|
||||
image_url: { url }
|
||||
}))
|
||||
];
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: content
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const comparison = await compareImages(
|
||||
[
|
||||
'https://example.com/before.jpg',
|
||||
'https://example.com/after.jpg'
|
||||
],
|
||||
'Compare these two images and describe the differences'
|
||||
);
|
||||
```
|
||||
|
||||
### Base64 Image Support
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
import fs from 'fs';
|
||||
|
||||
async function analyzeLocalImage(imagePath, question) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
// Read image file and convert to base64
|
||||
const imageBuffer = fs.readFileSync(imagePath);
|
||||
const base64Image = imageBuffer.toString('base64');
|
||||
const mimeType = imagePath.endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: question
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: `data:${mimeType};base64,${base64Image}`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content;
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Use Cases
|
||||
|
||||
### Conversational Vision Chat
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
class VisionChatSession {
|
||||
constructor() {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.zai = await ZAI.create();
|
||||
}
|
||||
|
||||
async addImage(imageUrl, initialQuestion) {
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: initialQuestion
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: imageUrl }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return this.getResponse();
|
||||
}
|
||||
|
||||
async followUp(question) {
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: question
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return this.getResponse();
|
||||
}
|
||||
|
||||
async getResponse() {
|
||||
const response = await this.zai.chat.completions.createVision({
|
||||
messages: this.messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const assistantMessage = response.choices[0]?.message?.content;
|
||||
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: assistantMessage
|
||||
});
|
||||
|
||||
return assistantMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const session = new VisionChatSession();
|
||||
await session.initialize();
|
||||
|
||||
const initial = await session.addImage(
|
||||
'https://example.com/chart.jpg',
|
||||
'What does this chart show?'
|
||||
);
|
||||
console.log('Initial analysis:', initial);
|
||||
|
||||
const followup = await session.followUp('What are the key trends?');
|
||||
console.log('Follow-up:', followup);
|
||||
```
|
||||
|
||||
### Image Classification and Tagging
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function classifyImage(imageUrl) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const prompt = `Analyze this image and provide:
|
||||
1. Main subject/category
|
||||
2. Key objects detected
|
||||
3. Scene description
|
||||
4. Suggested tags (comma-separated)
|
||||
|
||||
Format your response as JSON.`;
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: prompt
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: imageUrl }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
|
||||
try {
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
return { rawResponse: content };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OCR and Text Extraction
|
||||
|
||||
```javascript
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function extractText(imageUrl) {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'Extract all text from this image. Preserve the layout and formatting as much as possible.'
|
||||
},
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: imageUrl }
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Image Quality and Size
|
||||
- Use high-quality images for better analysis results
|
||||
- Optimize image size to balance quality and processing speed
|
||||
- Supported formats: JPEG, PNG, WebP
|
||||
|
||||
### 2. Prompt Engineering
|
||||
- Be specific about what information you need from the image
|
||||
- Structure complex requests with numbered lists or bullet points
|
||||
- Provide context about the image type (photo, diagram, chart, etc.)
|
||||
|
||||
### 3. Error Handling
|
||||
```javascript
|
||||
async function safeVisionChat(imageUrl, question) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: question },
|
||||
{ type: 'image_url', image_url: { url: imageUrl } }
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
content: response.choices[0]?.message?.content
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Vision chat error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Performance Optimization
|
||||
- Cache SDK instance creation when processing multiple images
|
||||
- Use appropriate image formats (JPEG for photos, PNG for diagrams)
|
||||
- Consider image preprocessing for large batches
|
||||
|
||||
### 5. Security Considerations
|
||||
- Validate image URLs before processing
|
||||
- Sanitize user-provided image data
|
||||
- Implement rate limiting for public-facing APIs
|
||||
- Never expose SDK credentials in client-side code
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
1. **Product Analysis**: Analyze product images for e-commerce applications
|
||||
2. **Document Understanding**: Extract information from receipts, invoices, forms
|
||||
3. **Medical Imaging**: Assist in preliminary analysis (with appropriate disclaimers)
|
||||
4. **Quality Control**: Detect defects or anomalies in manufacturing
|
||||
5. **Content Moderation**: Analyze images for policy compliance
|
||||
6. **Accessibility**: Generate alt text for images automatically
|
||||
7. **Visual Search**: Understand and categorize images for search functionality
|
||||
|
||||
## Integration Examples
|
||||
|
||||
### Express.js API Endpoint
|
||||
|
||||
```javascript
|
||||
import express from 'express';
|
||||
import ZAI from 'z-ai-web-dev-sdk';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
let zaiInstance;
|
||||
|
||||
// Initialize SDK once
|
||||
async function initZAI() {
|
||||
zaiInstance = await ZAI.create();
|
||||
}
|
||||
|
||||
app.post('/api/analyze-image', async (req, res) => {
|
||||
try {
|
||||
const { imageUrl, question } = req.body;
|
||||
|
||||
if (!imageUrl || !question) {
|
||||
return res.status(400).json({
|
||||
error: 'imageUrl and question are required'
|
||||
});
|
||||
}
|
||||
|
||||
const response = await zaiInstance.chat.completions.createVision({
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: question },
|
||||
{ type: 'image_url', image_url: { url: imageUrl } }
|
||||
]
|
||||
}
|
||||
],
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
analysis: response.choices[0]?.message?.content
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
initZAI().then(() => {
|
||||
app.listen(3000, () => {
|
||||
console.log('Vision chat API running on port 3000');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "SDK must be used in backend"
|
||||
- **Solution**: Ensure z-ai-web-dev-sdk is only imported and used in server-side code
|
||||
|
||||
**Issue**: Image not loading or being analyzed
|
||||
- **Solution**: Verify the image URL is accessible and returns a valid image format
|
||||
|
||||
**Issue**: Poor analysis quality
|
||||
- **Solution**: Provide more specific prompts and ensure image quality is sufficient
|
||||
|
||||
**Issue**: Slow response times
|
||||
- **Solution**: Optimize image size and consider caching frequently analyzed images
|
||||
|
||||
## Remember
|
||||
|
||||
- Always use z-ai-web-dev-sdk in backend code only
|
||||
- The SDK is already installed - import as shown in examples
|
||||
- Structure prompts clearly for best results
|
||||
- Handle errors gracefully in production applications
|
||||
- Consider user privacy when processing images
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
import ZAI, { VisionMessage } from 'z-ai-web-dev-sdk';
|
||||
|
||||
async function main(imageUrl: string, prompt: string) {
|
||||
try {
|
||||
const zai = await ZAI.create();
|
||||
|
||||
const messages: VisionMessage[] = [
|
||||
{
|
||||
role: 'assistant',
|
||||
content: [
|
||||
{ type: 'text', text: 'Output only text, no markdown.' }
|
||||
]
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: prompt },
|
||||
{ type: 'image_url', image_url: { url: imageUrl } }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// const messages: VisionMessage[] = [
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: [
|
||||
// { type: 'text', text: prompt },
|
||||
// { type: 'video_url', video_url: { url: imageUrl } }
|
||||
// ]
|
||||
// }
|
||||
// ];
|
||||
|
||||
// const messages: VisionMessage[] = [
|
||||
// {
|
||||
// role: 'user',
|
||||
// content: [
|
||||
// { type: 'text', text: prompt },
|
||||
// { type: 'file_url', file_url: { url: imageUrl } }
|
||||
// ]
|
||||
// }
|
||||
// ];
|
||||
|
||||
const response = await zai.chat.completions.createVision({
|
||||
model: 'glm-4.6v',
|
||||
messages,
|
||||
thinking: { type: 'disabled' }
|
||||
});
|
||||
|
||||
const reply = response.choices?.[0]?.message?.content;
|
||||
console.log('Vision model reply:');
|
||||
console.log(reply ?? JSON.stringify(response, null, 2));
|
||||
} catch (err: any) {
|
||||
console.error('Vision chat failed:', err?.message || err);
|
||||
}
|
||||
}
|
||||
|
||||
main("https://cdn.bigmodel.cn/static/logo/register.png", "Please describe this image.");
|
||||
Executable
+328
@@ -0,0 +1,328 @@
|
||||
---
|
||||
name: Agent Browser
|
||||
description: A fast Rust-based headless browser automation CLI with Node.js fallback that enables AI agents to navigate, click, type, and snapshot pages via structured commands.
|
||||
read_when:
|
||||
- Automating web interactions
|
||||
- Extracting structured data from pages
|
||||
- Filling forms programmatically
|
||||
- Testing web UIs
|
||||
metadata: {"clawdbot":{"emoji":"🌐","requires":{"bins":["node","npm"]}}}
|
||||
allowed-tools: Bash(agent-browser:*)
|
||||
---
|
||||
|
||||
# Browser Automation with agent-browser
|
||||
|
||||
## Installation
|
||||
|
||||
### npm recommended
|
||||
|
||||
```bash
|
||||
npm install -g agent-browser
|
||||
agent-browser install
|
||||
agent-browser install --with-deps
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vercel-labs/agent-browser
|
||||
cd agent-browser
|
||||
pnpm install
|
||||
pnpm build
|
||||
agent-browser install
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to page
|
||||
agent-browser snapshot -i # Get interactive elements with refs
|
||||
agent-browser click @e1 # Click element by ref
|
||||
agent-browser fill @e2 "text" # Fill input by ref
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
## Core workflow
|
||||
|
||||
1. Navigate: `agent-browser open <url>`
|
||||
2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`)
|
||||
3. Interact using refs from the snapshot
|
||||
4. Re-snapshot after navigation or significant DOM changes
|
||||
|
||||
## Commands
|
||||
|
||||
### Navigation
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser
|
||||
```
|
||||
|
||||
### Snapshot (page analysis)
|
||||
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
```
|
||||
|
||||
### Interactions (use @refs from snapshot)
|
||||
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser focus @e1 # Focus element
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser keydown Shift # Hold key down
|
||||
agent-browser keyup Shift # Release key
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
agent-browser scrollintoview @e1 # Scroll element into view
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
```
|
||||
|
||||
### Get information
|
||||
|
||||
```bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get html @e1 # Get innerHTML
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get attr @e1 href # Get attribute
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get count ".item" # Count matching elements
|
||||
agent-browser get box @e1 # Get bounding box
|
||||
```
|
||||
|
||||
### Check state
|
||||
|
||||
```bash
|
||||
agent-browser is visible @e1 # Check if visible
|
||||
agent-browser is enabled @e1 # Check if enabled
|
||||
agent-browser is checked @e1 # Check if checked
|
||||
```
|
||||
|
||||
### Screenshots & PDF
|
||||
|
||||
```bash
|
||||
agent-browser screenshot # Screenshot to stdout
|
||||
agent-browser screenshot path.png # Save to file
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
```
|
||||
|
||||
### Video recording
|
||||
|
||||
```bash
|
||||
agent-browser record start ./demo.webm # Start recording (uses current URL + state)
|
||||
agent-browser click @e1 # Perform actions
|
||||
agent-browser record stop # Stop and save video
|
||||
agent-browser record restart ./take2.webm # Stop current + start new recording
|
||||
```
|
||||
|
||||
Recording creates a fresh context but preserves cookies/storage from your session. If no URL is provided, it automatically returns to your current page. For smooth demos, explore first, then start recording.
|
||||
|
||||
### Wait
|
||||
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text
|
||||
agent-browser wait --url "/dashboard" # Wait for URL pattern
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --fn "window.ready" # Wait for JS condition
|
||||
```
|
||||
|
||||
### Mouse control
|
||||
|
||||
```bash
|
||||
agent-browser mouse move 100 200 # Move mouse
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
```
|
||||
|
||||
### Semantic locators (alternative to refs)
|
||||
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find nth 2 "a" text
|
||||
```
|
||||
|
||||
### Browser settings
|
||||
|
||||
```bash
|
||||
agent-browser set viewport 1920 1080 # Set viewport size
|
||||
agent-browser set device "iPhone 14" # Emulate device
|
||||
agent-browser set geo 37.7749 -122.4194 # Set geolocation
|
||||
agent-browser set offline on # Toggle offline mode
|
||||
agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
|
||||
agent-browser set credentials user pass # HTTP basic auth
|
||||
agent-browser set media dark # Emulate color scheme
|
||||
```
|
||||
|
||||
### Cookies & Storage
|
||||
|
||||
```bash
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set name value # Set cookie
|
||||
agent-browser cookies clear # Clear cookies
|
||||
agent-browser storage local # Get all localStorage
|
||||
agent-browser storage local key # Get specific key
|
||||
agent-browser storage local set k v # Set value
|
||||
agent-browser storage local clear # Clear all
|
||||
```
|
||||
|
||||
### Network
|
||||
|
||||
```bash
|
||||
agent-browser network route <url> # Intercept requests
|
||||
agent-browser network route <url> --abort # Block requests
|
||||
agent-browser network route <url> --body '{}' # Mock response
|
||||
agent-browser network unroute [url] # Remove routes
|
||||
agent-browser network requests # View tracked requests
|
||||
agent-browser network requests --filter api # Filter requests
|
||||
```
|
||||
|
||||
### Tabs & Windows
|
||||
|
||||
```bash
|
||||
agent-browser tab # List tabs
|
||||
agent-browser tab new [url] # New tab
|
||||
agent-browser tab 2 # Switch to tab
|
||||
agent-browser tab close # Close tab
|
||||
agent-browser window new # New window
|
||||
```
|
||||
|
||||
### Frames
|
||||
|
||||
```bash
|
||||
agent-browser frame "#iframe" # Switch to iframe
|
||||
agent-browser frame main # Back to main frame
|
||||
```
|
||||
|
||||
### Dialogs
|
||||
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
```bash
|
||||
agent-browser eval "document.title" # Run JavaScript
|
||||
```
|
||||
|
||||
### State management
|
||||
|
||||
```bash
|
||||
agent-browser state save auth.json # Save session state
|
||||
agent-browser state load auth.json # Load saved state
|
||||
```
|
||||
|
||||
## Example: Form submission
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/form
|
||||
agent-browser snapshot -i
|
||||
# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3]
|
||||
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i # Check result
|
||||
```
|
||||
|
||||
## Example: Authentication with saved state
|
||||
|
||||
```bash
|
||||
# Login once
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "username"
|
||||
agent-browser fill @e2 "password"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "/dashboard"
|
||||
agent-browser state save auth.json
|
||||
|
||||
# Later sessions: load saved state
|
||||
agent-browser state load auth.json
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
## Sessions (parallel browsers)
|
||||
|
||||
```bash
|
||||
agent-browser --session test1 open site-a.com
|
||||
agent-browser --session test2 open site-b.com
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
## JSON output (for parsing)
|
||||
|
||||
Add `--json` for machine-readable output:
|
||||
|
||||
```bash
|
||||
agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
agent-browser open example.com --headed # Show browser window
|
||||
agent-browser console # View console messages
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
agent-browser record start ./debug.webm # Record from current page
|
||||
agent-browser record stop # Save recording
|
||||
agent-browser --cdp 9222 snapshot # Connect via CDP
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If the command is not found on Linux ARM64, use the full path in the bin folder.
|
||||
- If an element is not found, use snapshot to find the correct ref.
|
||||
- If the page is not loaded, add a wait command after navigation.
|
||||
- Use --headed to see the browser window for debugging.
|
||||
|
||||
## Options
|
||||
|
||||
- --session <name> uses an isolated session.
|
||||
- --json provides JSON output.
|
||||
- --full takes a full page screenshot.
|
||||
- --headed shows the browser window.
|
||||
- --timeout sets the command timeout in milliseconds.
|
||||
- --cdp <port> connects via Chrome DevTools Protocol.
|
||||
|
||||
## Notes
|
||||
|
||||
- Refs are stable per page load but change on navigation.
|
||||
- Always snapshot after navigation to get new refs.
|
||||
- Use fill instead of type for input fields to ensure existing text is cleared.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
- Skill issues: Open an issue at https://github.com/TheSethRose/Agent-Browser-CLI
|
||||
- agent-browser CLI issues: Open an issue at https://github.com/vercel-labs/agent-browser
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: ai-news-collector
|
||||
description: AI 新闻聚合与热度排序工具。当用户询问 AI 领域最新动态时触发,如:"今天有什么 AI 新闻?""总结一下这周的 AI 动态""最近有什么火的 AI 产品?""AI 圈最近在讨论什么?"。覆盖:新产品发布、研究论文、行业动态、融资新闻、开源项目更新、社区病毒传播现象、AI 工具/Agent 热门项目。输出中文摘要列表,按热度排序,附带原文链接。
|
||||
---
|
||||
|
||||
# AI News Collector
|
||||
|
||||
收集、聚合并按热度排序 AI 领域新闻。
|
||||
|
||||
## 核心原则
|
||||
|
||||
**不要只搜"AI news today"。** 泛搜索返回的是 SEO 聚合页和趋势预测文章,会系统性遗漏社区级病毒传播现象(如开源工具爆火、Meme 级事件)。必须用多维度、分层搜索策略。
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 1. 多维度分层搜索(最少 8 次,建议 10-12 次)
|
||||
|
||||
按以下 **6 个维度** 依次执行搜索,每个维度至少 1 次:
|
||||
|
||||
#### 维度 A:周报/Newsletter 聚合(最优先 🔑)
|
||||
|
||||
这是信息密度最高的来源,一篇文章可覆盖 10+ 条新闻。
|
||||
|
||||
```
|
||||
搜索词:
|
||||
- "last week in AI" [当前月份年份]
|
||||
- "AI weekly roundup" [当前月份年份]
|
||||
- "the batch AI newsletter"
|
||||
- site:substack.com AI news [当前月份]
|
||||
```
|
||||
|
||||
发现周报后,用 web_fetch 获取全文,从中提取所有新闻线索。
|
||||
|
||||
#### 维度 B:社区热度/病毒传播(关键维度 🔑)
|
||||
|
||||
捕捉自下而上的社区爆款,这类信息泛搜索几乎无法触达。
|
||||
|
||||
```
|
||||
搜索词:
|
||||
- "viral AI tool" OR "viral AI agent"
|
||||
- "AI trending" site:reddit.com OR site:news.ycombinator.com
|
||||
- "GitHub trending AI" OR "AI open source trending"
|
||||
- AI buzzing OR "everyone is talking about" AI
|
||||
- "most popular AI" this week
|
||||
```
|
||||
|
||||
#### 维度 C:产品发布与模型更新
|
||||
|
||||
```
|
||||
搜索词:
|
||||
- "AI model release" OR "LLM launch" [当前月份]
|
||||
- "AI product launch" [当前月份年份]
|
||||
- OpenAI OR Anthropic OR Google OR Meta AI announcement
|
||||
- "大模型 发布" OR "AI 新产品"
|
||||
```
|
||||
|
||||
#### 维度 D:融资与商业
|
||||
|
||||
```
|
||||
搜索词:
|
||||
- "AI startup funding" [当前月份年份]
|
||||
- "AI acquisition" OR "AI IPO"
|
||||
- "AI 融资" OR "人工智能投资"
|
||||
```
|
||||
|
||||
#### 维度 E:研究突破
|
||||
|
||||
```
|
||||
搜索词:
|
||||
- "AI breakthrough" OR "AI paper" [当前月份]
|
||||
- "state of the art" machine learning
|
||||
- "AI 论文" OR "机器学习突破"
|
||||
```
|
||||
|
||||
#### 维度 F:监管与政策
|
||||
|
||||
```
|
||||
搜索词:
|
||||
- "AI regulation" OR "AI policy" [当前月份年份]
|
||||
- "AI law" OR "AI governance"
|
||||
- "AI 监管" OR "人工智能法案"
|
||||
```
|
||||
|
||||
### 2. 交叉验证与补漏
|
||||
|
||||
初轮搜索完成后,检查是否有遗漏:
|
||||
|
||||
- 如果 Newsletter 中提到了某个项目/事件但初轮搜索未覆盖 → 对该项目专项搜索
|
||||
- 如果同一事件被 3+ 个不同来源提及 → 大概率是热点,深入搜索获取更多细节
|
||||
- 如果中文媒体和英文媒体的热点完全不同 → 两边都要覆盖
|
||||
|
||||
### 3. 搜索关键词设计原则(反模式清单)
|
||||
|
||||
| ❌ 不要这样搜 | ✅ 应该这样搜 | 原因 |
|
||||
|---|---|---|
|
||||
| "AI news today February 2026" | "AI weekly roundup February 2026" | 前者返回聚合页,后者返回策划内容 |
|
||||
| "AI news today" | "viral AI tool" + "AI model release" 分开搜 | 泛搜无法覆盖社区现象 |
|
||||
| "artificial intelligence breaking news" | 按维度分类搜索 | 过于宽泛,返回噪音 |
|
||||
| 搜索词中加具体年月日 | 用 "this week" "today" "latest" | 日期反而会偏向预测/展望文章 |
|
||||
| 只搜 3 次就开始写 | 至少 8 次,覆盖 6 个维度 | 3 次搜索覆盖率不到 30% |
|
||||
|
||||
### 4. 热度综合判断
|
||||
|
||||
基于以下信号评估每条新闻热度(1-5 星):
|
||||
|
||||
| 信号 | 权重 | 说明 |
|
||||
|------|------|------|
|
||||
| 多家媒体报道同一事件 | ⭐⭐⭐ 高 | 3+ 来源 = 确认热点 |
|
||||
| 社区病毒传播证据 | ⭐⭐⭐ 高 | GitHub star 暴涨、Twitter 刷屏、HN 首页 |
|
||||
| 来自权威来源(顶会、大厂官宣) | ⭐⭐⭐ 高 | 但注意大厂 PR 不等于真热点 |
|
||||
| 实际用户体验分享 | ⭐⭐ 中 | 有人真的在用 > 只是发布了 |
|
||||
| 技术突破性/影响范围 | ⭐⭐ 中 | |
|
||||
| 争议性(安全、伦理讨论) | ⭐⭐ 中 | 争议往往说明影响力大 |
|
||||
| 时效性(越新越热) | ⭐ 中低 | 辅助排序 |
|
||||
|
||||
### 5. 输出格式
|
||||
|
||||
按热度降序排列,输出 **15-25 条**新闻:
|
||||
|
||||
```
|
||||
## 🔥 AI 新闻速递(YYYY-MM-DD)
|
||||
|
||||
### ⭐⭐⭐⭐⭐ 热度最高
|
||||
|
||||
1. **[新闻标题]**
|
||||
> 一句话摘要(不超过 50 字)
|
||||
> 🔗 [来源名称](URL)
|
||||
|
||||
### ⭐⭐⭐⭐ 高热度
|
||||
|
||||
2. ...
|
||||
|
||||
### ⭐⭐⭐ 中等热度
|
||||
|
||||
...
|
||||
|
||||
---
|
||||
📊 本次共收集 XX 条新闻 | 搜索 XX 次 | 覆盖维度:A/B/C/D/E/F | 更新时间:HH:MM
|
||||
```
|
||||
|
||||
### 6. 去重与合并
|
||||
|
||||
- 同一事件被多家报道时,合并为一条,选择最权威/详细的来源
|
||||
- 在摘要中注明"多家媒体报道"以体现热度
|
||||
- 改名/更名的项目视为同一事件(如 Clawdbot → Moltbot → OpenClaw)
|
||||
|
||||
## 推荐新闻源
|
||||
|
||||
详见 [references/sources.md](references/sources.md)。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 优先使用 HTTPS 链接
|
||||
- 遇到付费墙/无法访问的内容,标注"需订阅"
|
||||
- 保持客观,不对新闻内容做主观评价
|
||||
- 搜索不足 8 次不要开始输出
|
||||
- 如果某个维度搜索结果为空,换关键词再搜一次
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7fr165ff9vkkwsqyqrq2nwas80t4ev",
|
||||
"slug": "ai-news-collectors",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1770615394344
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
# AI 新闻源推荐列表
|
||||
|
||||
## Newsletter / 周报(信息密度最高 🔑)
|
||||
|
||||
| 来源 | 网址 | 特点 |
|
||||
|------|------|------|
|
||||
| Last Week in AI | lastweekin.ai / medium.com/last-week-in-ai | 每周最全面的 AI 新闻汇总 |
|
||||
| The Batch (Andrew Ng) | deeplearning.ai/the-batch | 权威周报 |
|
||||
| Import AI (Jack Clark) | importai.net | Anthropic 联创的 AI 周报 |
|
||||
| Platformer (Casey Newton) | platformer.news | 科技深度分析,覆盖 AI |
|
||||
| The Neuron | theneurondaily.com | 每日 AI 简报 |
|
||||
| Ben's Bites | bensbites.com | AI 产品/工具为主 |
|
||||
| AI Tidbits | aitidbits.substack.com | AI 动态精选 |
|
||||
| TLDR AI | tldr.tech/ai | 每日 AI 简报 |
|
||||
| Interconnects | interconnects.ai | 深度技术分析 |
|
||||
|
||||
## 关键 Substack / 独立博客
|
||||
|
||||
| 来源 | 网址 | 特点 |
|
||||
|------|------|------|
|
||||
| Gary Marcus | garymarcus.substack.com | AI 批评性分析,常首发安全/争议话题 |
|
||||
| Simon Willison | simonwillison.net | LLM 安全、工具生态,社区现象第一手报道 |
|
||||
| Ethan Mollick | oneusefulthing.substack.com | Wharton 教授,AI 应用洞察 |
|
||||
| Lenny's Newsletter | lennysnewsletter.com | AI 产品/增长 |
|
||||
| Understanding AI | understandingai.org | 趋势分析与预测 |
|
||||
| Nathan Lebenz (Cognitive Revolution) | cognitiverevolution.substack.com | AI 深度访谈 |
|
||||
|
||||
## 国际主流媒体
|
||||
|
||||
| 来源 | 网址 | 侧重 |
|
||||
|------|------|------|
|
||||
| TechCrunch AI | techcrunch.com/category/artificial-intelligence | 产品、融资 |
|
||||
| The Verge AI | theverge.com/ai-artificial-intelligence | 产品、行业 |
|
||||
| Ars Technica | arstechnica.com/ai | 深度分析 |
|
||||
| VentureBeat AI | venturebeat.com/ai | 企业 AI |
|
||||
| MIT Tech Review | technologyreview.com/artificial-intelligence | 研究、趋势 |
|
||||
| Wired AI | wired.com/tag/artificial-intelligence | 行业影响 |
|
||||
| CNBC Tech | cnbc.com/technology | 商业+科技交叉 |
|
||||
| Scientific American | scientificamerican.com | 科学视角 AI |
|
||||
| The Information | theinformation.com | 深度报道(付费) |
|
||||
|
||||
## 国内媒体
|
||||
|
||||
| 来源 | 网址 | 侧重 |
|
||||
|------|------|------|
|
||||
| 机器之心 | jiqizhixin.com | 技术、论文 |
|
||||
| 量子位 | qbitai.com | 产品、行业 |
|
||||
| 36氪 AI | 36kr.com/information/AI | 融资、产品 |
|
||||
| InfoQ AI | infoq.cn/topic/AI | 技术实践 |
|
||||
| 新智元 | xinzhiyuan.com | 行业动态 |
|
||||
| AI 科技评论 | leiphone.com/category/ai | 技术、产品 |
|
||||
|
||||
## 社区与论坛(捕捉病毒传播 🔑)
|
||||
|
||||
| 来源 | 网址 | 特点 |
|
||||
|------|------|------|
|
||||
| Hacker News | news.ycombinator.com | 技术讨论热度,开源项目首发 |
|
||||
| Reddit r/MachineLearning | reddit.com/r/MachineLearning | 学术前沿 |
|
||||
| Reddit r/artificial | reddit.com/r/artificial | 综合讨论 |
|
||||
| Reddit r/LocalLLaMA | reddit.com/r/LocalLLaMA | 本地模型、开源工具热度 |
|
||||
| Reddit r/singularity | reddit.com/r/singularity | AI 社区热议 |
|
||||
| Twitter/X | twitter.com | 实时动态,病毒传播首发地 |
|
||||
| 即刻 AI 圈子 | okjike.com | 国内社区讨论 |
|
||||
| V2EX | v2ex.com | 开发者视角 |
|
||||
| 知乎 | zhihu.com | 深度技术讨论 |
|
||||
|
||||
## 安全与批评视角
|
||||
|
||||
| 来源 | 网址 | 特点 |
|
||||
|------|------|------|
|
||||
| Palo Alto Networks Blog | paloaltonetworks.com/blog | AI 安全预警 |
|
||||
| 1Password Blog | 1password.com/blog | Agent 安全分析 |
|
||||
| Trail of Bits | blog.trailofbits.com | AI 安全研究 |
|
||||
|
||||
## 学术与研究
|
||||
|
||||
| 来源 | 网址 | 特点 |
|
||||
|------|------|------|
|
||||
| arXiv CS.AI | arxiv.org/list/cs.AI/recent | 最新论文 |
|
||||
| arXiv CS.LG | arxiv.org/list/cs.LG/recent | 机器学习论文 |
|
||||
| Papers With Code | paperswithcode.com | 论文+代码 |
|
||||
| Google AI Blog | ai.googleblog.com | 谷歌研究 |
|
||||
| OpenAI Blog | openai.com/blog | OpenAI 动态 |
|
||||
| Anthropic News | anthropic.com/news | Anthropic 动态 |
|
||||
| DeepMind Blog | deepmind.com/blog | DeepMind 研究 |
|
||||
| Meta AI | ai.meta.com/blog | Meta 研究 |
|
||||
| Hugging Face Blog | huggingface.co/blog | 开源生态 |
|
||||
|
||||
## 开源项目追踪
|
||||
|
||||
| 来源 | 网址 | 特点 |
|
||||
|------|------|------|
|
||||
| GitHub Trending | github.com/trending | 热门项目,必查 |
|
||||
| Product Hunt AI | producthunt.com/topics/artificial-intelligence | 新产品发布 |
|
||||
| Awesome LLM | github.com/Hannibal046/Awesome-LLM | LLM 资源汇总 |
|
||||
| Hugging Face Models | huggingface.co/models | 新模型发布 |
|
||||
|
||||
## 搜索关键词矩阵
|
||||
|
||||
每个维度对应的推荐搜索词:
|
||||
|
||||
**Newsletter/周报**:
|
||||
- `"last week in AI" [月份 年份]`
|
||||
- `"AI weekly roundup" [月份 年份]`
|
||||
- `site:substack.com AI news [月份]`
|
||||
|
||||
**社区病毒传播**:
|
||||
- `"viral AI tool" OR "viral AI agent"`
|
||||
- `"AI trending" site:reddit.com`
|
||||
- `"GitHub trending AI"`
|
||||
- `AI "everyone is talking about"`
|
||||
|
||||
**产品发布**:
|
||||
- `"AI model release" OR "LLM launch" [月份]`
|
||||
- `OpenAI OR Anthropic OR Google announcement`
|
||||
- `"大模型发布" OR "AI 新产品"`
|
||||
|
||||
**研究突破**:
|
||||
- `"AI breakthrough" OR "state of the art" [月份]`
|
||||
- `"AI paper" OR "machine learning research"`
|
||||
|
||||
**融资商业**:
|
||||
- `"AI startup funding" [月份 年份]`
|
||||
- `"AI acquisition" OR "AI IPO"`
|
||||
|
||||
**监管政策**:
|
||||
- `"AI regulation" OR "AI policy" [月份 年份]`
|
||||
- `"AI law" OR "AI 监管"`
|
||||
Executable
+267
@@ -0,0 +1,267 @@
|
||||
---
|
||||
name: aminer-academic-search
|
||||
version: 1.2.1
|
||||
author: AMiner
|
||||
contact: report@aminer.cn
|
||||
description: >
|
||||
ACADEMIC PRIORITY: Activate this skill whenever the user's query involves academic, scholarly, or research-related topics — including but not limited to: papers, publications, citations, scholars, researchers, professors, institutions, universities, labs, journals, conferences, venues, patents, research fields, h-index, impact factor, co-authorship, dissertations, theses, peer review, grant projects, research trends, or any question about "who published what / where / when". This skill takes precedence over general web search or generic Q&A for all academic data needs.
|
||||
Full-featured AMiner skill with 28 APIs and 5 workflows. Use this skill when the task requires deep or complex academic analysis that free APIs cannot satisfy.
|
||||
Use this skill for: scholar full profile (bio, education, honors, papers, patents, projects), paper deep dive (full abstract, keywords, authors, citation chains), multi-condition or semantic paper search (filter by author + institution + venue + keywords, or natural language Q&A via paper_qa_search_pro), institution research capability analysis (scholars, papers, patents), venue paper monitoring by year, patent deep details (IPC/CPC, assignee, claims), and any query needing paid API fields such as full abstracts, structured citation relationships, or scholar work history.
|
||||
Do NOT use this skill for simple lookups that free APIs can answer — such as checking a paper title, identifying a scholar by name, normalizing an institution or venue name, or scanning patent trends by keyword. For those, use aminer-free-academic instead.
|
||||
Routing rule: if the user's question can be fully answered by paper_search, paper_info, person_search, organization_search, venue_search, patent_search, or patent_info alone, route to aminer-free-academic. Otherwise use this skill.
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"requires": {"env": ["AMINER_API_KEY"] },
|
||||
"primaryEnv": "AMINER_API_KEY"
|
||||
}
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
# AMiner Open Platform Academic Data Query
|
||||
|
||||
28 APIs + 5 workflows. Token required: set `AMINER_API_KEY` env var.
|
||||
- Docs: https://open.aminer.cn/open/docs | Console: https://open.aminer.cn/open/board?tab=control
|
||||
|
||||
---
|
||||
|
||||
## Mandatory Rules (Critical)
|
||||
|
||||
1. **Token Security**: Only check whether `AMINER_API_KEY` exists; never expose the token in plain text anywhere.
|
||||
2. **Cost Control**: Prefer optimal combined queries; never do indiscriminate full-detail retrieval. Default to top 10 details when the user has not specified a count.
|
||||
3. **Free-First**: Prefer free APIs unless the user explicitly requires deeper fields; only upgrade to paid APIs when free ones cannot satisfy the need.
|
||||
4. **Result Links**: Always append an accessible URL after each entity in the output.
|
||||
5. **Disambiguation**: Scholar ambiguity → filter by `org`/`org_id` or ask user to confirm. Org ambiguity → use `org_disambiguate_pro`. Paper ambiguity → cross-check `year` + `venue_name` + `first_author`.
|
||||
6. **Cost Report**: After completing all API calls, always output a cost summary to the user showing: each API called, its unit price, number of calls, and the total cost. Format example: `[Cost] ¥X.XX total, N API calls (api_a: ¥X.XX × N, api_b: Free × N)`.
|
||||
7. **High-Cost Confirmation (≥ ¥5)**: Before executing a workflow or call chain whose estimated total cost is ¥5.00 or more, **stop and ask the user for confirmation** first. Show the planned call chain, estimated cost per step, and the total. Only proceed after the user explicitly agrees. This applies to both predefined workflows (e.g., Scholar Profile ~¥6.00) and ad-hoc multi-step plans.
|
||||
|
||||
Entity URL templates (mandatory):
|
||||
- Paper: `https://www.aminer.cn/pub/{paper_id}`
|
||||
- Scholar: `https://www.aminer.cn/profile/{scholar_id}`
|
||||
- Patent: `https://www.aminer.cn/patent/{patent_id}`
|
||||
- Journal: `https://www.aminer.cn/open/journal/detail/{journal_id}`
|
||||
|
||||
---
|
||||
|
||||
## Token Check (Required)
|
||||
|
||||
Check `AMINER_API_KEY` exists before any API call. Never expose token in plain text.
|
||||
|
||||
```bash
|
||||
[ -z "${AMINER_API_KEY+x}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"
|
||||
```
|
||||
|
||||
- If `${AMINER_API_KEY}` exists: proceed. If not: check `--token` parameter. If neither: **stop**, guide user to [Console](https://open.aminer.cn/open/board?tab=control) to generate one.
|
||||
- If the user provides `AMINER_API_KEY` inline (e.g. "My token is xxx"), accept it for the current session, but recommend setting it as an environment variable for better security.
|
||||
- Default headers: `Authorization: ${AMINER_API_KEY}`, `X-Platform: openclaw`, `Content-Type: application/json;charset=utf-8` (POST).
|
||||
|
||||
---
|
||||
|
||||
## Call Guardrails
|
||||
|
||||
1. Parameter names and types must match `references/api-catalog.md` exactly.
|
||||
2. `paper_info` is batch-only: `{"ids": [...]}`. `paper_detail` is single-paper only: one `id`. Never mix them.
|
||||
3. When multiple details are needed, filter with a low-cost API first, then fetch details for a small set.
|
||||
4. **Prefer `paper_qa_search_pro`; avoid legacy `paper_qa_search`.** For almost all paper Q&A / topic / filter searches, call `paper_qa_search_pro` first. Use legacy `paper_qa_search` **only** when the user explicitly needs `topic_high` / `topic_middle` / `topic_low` structured OR/AND mode that Pro does not support. Do not default to the legacy endpoint out of habit.
|
||||
|
||||
---
|
||||
|
||||
## Paper Search API Selection Guide
|
||||
|
||||
When the user says "search for papers", determine the goal first:
|
||||
|
||||
| API | Focus | Use Case | Cost |
|
||||
|---|---|---|---|
|
||||
| `paper_search` | Title search → `paper_id` | Known paper title, locate target | Free |
|
||||
| `paper_search_pro` | Multi-condition search (author/org/venue/keyword) | Topic search, sort by citations or year | ¥0.01 |
|
||||
| `paper_qa_search_pro` | Natural language Q&A + rich filters | **Default / prefer this** for semantic & filter search; card + cursor | ¥0.70 |
|
||||
| `paper_qa_search` | Legacy structured topic keywords | **Rarely**; only for `topic_high/middle/low` OR/AND | ¥0.05 |
|
||||
| `paper_list_by_keywords` | Multi-keyword batch retrieval | Batch thematic retrieval | ¥0.10 |
|
||||
| `paper_detail_by_condition` | Year + venue dimension | Journal annual monitoring | ¥0.20 |
|
||||
|
||||
Default routing:
|
||||
|
||||
1. **Known title**: `paper_search -> paper_detail -> paper_relation`
|
||||
2. **Conditional filtering**: `paper_search_pro -> paper_detail` (or `paper_qa_search_pro` when NL intent / citation-year soft filters help)
|
||||
3. **Natural language / topic Q&A**: **always prefer** `paper_qa_search_pro` → if empty, fall back to `paper_search_pro`. **Do not** start with legacy `paper_qa_search`.
|
||||
4. **Journal annual analysis**: `venue_search -> venue_paper_relation -> paper_detail_by_condition`
|
||||
|
||||
Key `paper_qa_search_pro` rules:
|
||||
- **Default choice** for natural-language paper search and multi-filter retrieval (`authors`/`author_ids`, `organizations`/`organization_ids`, `venues`/`venue_ids`, year/citation ranges, `all_terms`/`any_terms`/`exclude_terms`).
|
||||
- Page size is **fixed at 10**. Do not send `size`. Paginate with `next_cursor` → next request body is `{"cursor":"..."}` only.
|
||||
- `sort`: `relevance` / `balanced` / `recent` / `citation`. For “most cited” use `citation`; for “newest” use `recent`.
|
||||
- Response card fields only: `paper_id`, `title`, `title_zh`, `authors.name`/`name_zh`, `year`. Use `paper_detail` when full abstract/keywords are needed.
|
||||
- Always append `https://www.aminer.cn/pub/{paper_id}`.
|
||||
|
||||
Legacy `paper_qa_search` — use sparingly:
|
||||
- Call **only** when `topic_high` / `topic_middle` / `topic_low` structured OR/AND is explicitly required; otherwise use Pro.
|
||||
- `query` and `topic_high/topic_middle/topic_low` are **mutually exclusive**; do not pass both.
|
||||
- Supports `sci_flag`, `force_citation_sort`, `force_year_sort`, `author_id`, `org_id`, `venue_ids`.
|
||||
|
||||
Free-tier screening fields available:
|
||||
|
||||
- `paper_search`: `venue_name`, `first_author`, `n_citation_bucket`, `year`
|
||||
- `paper_info`: `abstract_slice`, `year`, `venue_id`, `author_count`
|
||||
- `person_search`: `interests`, `n_citation`, `org/org_id`
|
||||
- `organization_search`: `aliases`
|
||||
- `venue_search`: `aliases`, `venue_type`
|
||||
- `patent_search`: `inventor_name`, `app_year`, `pub_year`
|
||||
- `patent_info`: `app_year`, `pub_year`
|
||||
|
||||
---
|
||||
|
||||
## Handling Out-of-Workflow Requests
|
||||
|
||||
When the user's request falls outside the 5 workflows:
|
||||
|
||||
1. Read `references/api-catalog.md` to confirm available APIs, parameters, and response fields.
|
||||
2. Design the shortest viable call chain: locate ID → supplement details → expand relationships.
|
||||
3. Do not give up because "no existing workflow fits"; actively compose APIs based on `api-catalog`.
|
||||
|
||||
---
|
||||
|
||||
## 5 Combined Workflows
|
||||
|
||||
### Workflow 1: Scholar Profile (~¥6.00)
|
||||
|
||||
**Use Case**: Complete academic profile — bio, research interests, papers, patents, projects.
|
||||
**Cost note**: Full execution exceeds the ¥5 threshold → **must ask for user confirmation before proceeding** (Rule 7). Show the planned steps and cost. Confirm which sub-modules are needed; skip patents/projects if not requested.
|
||||
|
||||
**Call Chain:**
|
||||
```
|
||||
Scholar search (name → person_id)
|
||||
↓
|
||||
Parallel calls (pick as needed):
|
||||
├── Scholar details (bio/education/honors) ¥1.00
|
||||
├── Scholar portrait (interests/work history) ¥0.50
|
||||
├── Scholar papers (paper list) ¥1.50
|
||||
├── Scholar patents (patent list) ¥1.50
|
||||
└── Scholar projects (funding info) ¥1.50
|
||||
```
|
||||
|
||||
Fallback: if `paper_search` yields no results in sub-steps, fall back to `paper_search_pro`.
|
||||
|
||||
---
|
||||
|
||||
### Workflow 2: Paper Deep Dive (~¥0.12)
|
||||
|
||||
**Use Case**: Full paper information and citation chain from a title or keyword.
|
||||
|
||||
**Call Chain:**
|
||||
```
|
||||
Paper search / Paper search pro (title/keyword → paper_id)
|
||||
↓
|
||||
Paper details (abstract/authors/DOI/journal/year/keywords) ¥0.01
|
||||
↓
|
||||
Paper citations (cited papers → cited_ids) ¥0.10
|
||||
↓
|
||||
(Optional) Batch paper_info for cited papers Free
|
||||
```
|
||||
|
||||
Fallback: if `paper_search` yields no results, fall back to `paper_search_pro`.
|
||||
|
||||
---
|
||||
|
||||
### Workflow 3: Org Analysis (~¥0.81)
|
||||
|
||||
**Use Case**: Institution scholar size, paper output, patent count — for competitive research or partnership evaluation.
|
||||
|
||||
**Call Chain:**
|
||||
```
|
||||
Org disambiguation pro (raw string → org_id) ¥0.05
|
||||
↓
|
||||
Parallel calls:
|
||||
├── Org details (description/type) ¥0.01
|
||||
├── Org scholars (scholar list, 10/call) ¥0.50
|
||||
├── Org papers (paper list, 10/call) ¥0.10
|
||||
└── Org patents (patent IDs, up to 10,000) ¥0.10
|
||||
```
|
||||
|
||||
> If disambiguation pro returns no ID, fall back to `org_search` (free).
|
||||
|
||||
---
|
||||
|
||||
### Workflow 4: Venue Papers (~¥0.10 - ¥0.30)
|
||||
|
||||
**Use Case**: Track journal papers by year; useful for submission research or trend analysis.
|
||||
|
||||
**Call Chain:**
|
||||
```
|
||||
Venue search (name → venue_id) Free
|
||||
↓
|
||||
(Optional) Venue details (ISSN/type/abbreviation) ¥0.20
|
||||
↓
|
||||
Venue papers (venue_id + year → paper_id list) ¥0.10
|
||||
↓
|
||||
(Optional) Batch paper detail query
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Workflow 5: Patent Analysis (~¥0.02)
|
||||
|
||||
**Use Case**: Search patents in a technology domain, or retrieve a scholar's/institution's patent portfolio.
|
||||
|
||||
**Call Chain (standalone search):**
|
||||
```
|
||||
Patent search (query → patent_id) Free
|
||||
↓
|
||||
Patent info / Patent details Free / ¥0.01
|
||||
```
|
||||
|
||||
**Call Chain (via scholar/institution):**
|
||||
```
|
||||
Scholar search → Scholar patents (patent_id list)
|
||||
Org disambiguation → Org patents (patent_id list)
|
||||
↓
|
||||
Patent info / Patent details
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Individual API Quick Reference
|
||||
|
||||
> Full parameter docs: read `references/api-catalog.md`
|
||||
|
||||
| # | Title | Method | Price | API Path (Base: publicapi.chatglm.cn/chatglm_public/skill/aminer) |
|
||||
|---|------|------|------|------|
|
||||
| 1 | Paper QA Search Pro | POST | ¥0.70 | `/api/v3/paper/qa/searchPro` |
|
||||
| 2 | Paper QA Search (legacy) | POST | ¥0.05 | `/api/paper/qa/search` |
|
||||
| 3 | Scholar Search | POST | Free | `/api/person/search` |
|
||||
| 4 | Paper Search | GET | Free | `/api/paper/search` |
|
||||
| 5 | Paper Search Pro | GET | ¥0.01 | `/api/paper/search/pro` |
|
||||
| 6 | Patent Search | POST | Free | `/api/patent/search` |
|
||||
| 7 | Org Search | POST | Free | `/api/organization/search` |
|
||||
| 8 | Venue Search | POST | Free | `/api/venue/search` |
|
||||
| 9 | Scholar Details | GET | ¥1.00 | `/api/person/detail` |
|
||||
| 10 | Scholar Projects | GET | ¥1.50 | `/api/project/person/v3/open` |
|
||||
| 11 | Scholar Papers | GET | ¥1.50 | `/api/person/paper/relation` |
|
||||
| 12 | Scholar Patents | GET | ¥1.50 | `/api/person/patent/relation` |
|
||||
| 13 | Scholar Portrait | GET | ¥0.50 | `/api/person/figure` |
|
||||
| 14 | Paper Info | POST | Free | `/api/paper/info` |
|
||||
| 15 | Paper Details | GET | ¥0.01 | `/api/paper/detail` |
|
||||
| 16 | Paper Citations | GET | ¥0.10 | `/api/paper/relation` |
|
||||
| 17 | Patent Info | GET | Free | `/api/patent/info` |
|
||||
| 18 | Patent Details | GET | ¥0.01 | `/api/patent/detail` |
|
||||
| 19 | Org Details | POST | ¥0.01 | `/api/organization/detail` |
|
||||
| 20 | Org Patents | GET | ¥0.10 | `/api/organization/patent/relation` |
|
||||
| 21 | Org Scholars | GET | ¥0.50 | `/api/organization/person/relation` |
|
||||
| 22 | Org Papers | GET | ¥0.10 | `/api/organization/paper/relation` |
|
||||
| 23 | Venue Details | POST | ¥0.20 | `/api/venue/detail` |
|
||||
| 24 | Venue Papers | POST | ¥0.10 | `/api/venue/paper/relation` |
|
||||
| 25 | Org Disambiguation | POST | ¥0.01 | `/api/organization/na` |
|
||||
| 26 | Org Disambiguation Pro | POST | ¥0.05 | `/api/organization/na/pro` |
|
||||
| 27 | Paper Batch Query | GET | ¥0.10 | `/api/paper/list/citation/by/keywords` |
|
||||
| 28 | Paper Details by Year+Venue | GET | ¥0.20 | `/api/paper/platform/allpubs/more/detail/by/ts/org/venue` |
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Full API parameter documentation: read `references/api-catalog.md`
|
||||
- Optional Python client: `scripts/aminer_client.py`
|
||||
- Test cases: `evals/evals.json`
|
||||
- Official documentation: https://open.aminer.cn/open/docs
|
||||
- Console: https://open.aminer.cn/open/board?tab=control
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"skill_name": "aminer-academic-search",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "I'd like to understand Andrew Ng's complete academic profile, including his research interests, important papers he has published, any patents, and research projects he has participated in. Please use the AMiner API to look this up for me. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "The estimated cost (~¥6.00) exceeds the ¥5 threshold. Before executing, display the planned call chain and cost breakdown, and ask the user for confirmation. After the user confirms, run the scholar_profile workflow. After completion, output a cost summary.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Displayed the planned call chain with estimated cost BEFORE making paid API calls",
|
||||
"Estimated total cost exceeds ¥5, so asked the user for confirmation before proceeding",
|
||||
"After user confirmation, called Scholar Search, Scholar Details, Scholar Portrait, Scholar Papers, etc.",
|
||||
"Output contains the selected.name field with value Andrew Ng",
|
||||
"Output contains a cost summary showing each API called and total cost"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prompt": "Help me deep-dive into the paper 'Attention Is All You Need' — find it, retrieve its full details (abstract, authors, year), and then see what references it cites. My AMiner token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the paper_deep_dive workflow: search for the paper → retrieve details → retrieve citation relationships. Output the paper abstract, author list, DOI, and list of cited papers.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called a Paper Search API to locate the target paper",
|
||||
"Called the Paper Details API (paper_detail) to retrieve the abstract",
|
||||
"Called the Paper Citations API (paper_relation) to retrieve the citation list",
|
||||
"Output contains the detail field with an abstract sub-field",
|
||||
"Output contains the citations_count or citations_preview field"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prompt": "I need to analyze MIT's research strength — I'd like to know which prominent scholars are affiliated with them, what papers they have published, and what patents they hold. Please use the AMiner API to compile a report for me. Token: <YOUR_TOKEN>",
|
||||
"expected_output": "Run the org_analysis workflow: use Org Disambiguation Pro to find MIT's institution ID, then fetch institution details, scholar list, and paper list in parallel, and compile into a report.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called the Org Disambiguation Pro API (org_disambiguate_pro) to obtain the institution ID",
|
||||
"Called the Org Details API (org_detail) to retrieve MIT's description",
|
||||
"Called the Org Scholars API (org_person_relation) to retrieve the scholar list",
|
||||
"Called the Org Papers API (org_paper_relation) to retrieve the paper list",
|
||||
"Output contains the org_id field",
|
||||
"Output contains the scholars or papers field"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"prompt": "Help me look up Andrew Ng's publication trends",
|
||||
"expected_output": "Detect that the user has not provided a token; immediately stop all subsequent API calls and guide the user to obtain a token from the AMiner Console.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Clearly inform the user that a token is currently missing and AMiner API calls cannot continue",
|
||||
"No API calls were made",
|
||||
"Provide the console link https://open.aminer.cn/open/board?tab=control",
|
||||
"Prompt the user to continue after obtaining a token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"prompt": "Help me look up Fei-Fei Li from Stanford University on AMiner and give me her most-cited papers and her most recent papers. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Prefer paper_qa_search_pro with author_ids/organization_ids filters. First call with sort=citation, then with sort=recent. Each call returns up to 10 card items (fixed page size).",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called the Scholar Search API (person_search) to confirm Fei-Fei Li's author_id",
|
||||
"Called paper_qa_search_pro with author_ids (and organization_ids when available)",
|
||||
"Executed two separate queries: one with sort=citation and one with sort=recent",
|
||||
"Each result set returns paper_id/title(/title_zh)/year card fields and AMiner URLs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"prompt": "Help me check what papers were published in NeurIPS in 2023. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the venue_papers workflow: venue search to get venue_id, then venue_paper_relation with year=2023 to list papers.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called the Venue Search API (venue_search) to find NeurIPS venue_id",
|
||||
"Called the Venue Papers API (venue_paper_relation) with year=2023",
|
||||
"Output contains a list of paper titles and IDs",
|
||||
"Each paper entity has an AMiner URL appended"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"prompt": "Search for patents related to quantum computing chips and show me the details of the top results. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run patent_search to find patents, then patent_detail for the top results. Output patent titles, inventors, IPC codes, and abstracts.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called the Patent Search API (patent_search) with query about quantum computing",
|
||||
"Called the Patent Details API (patent_detail) for at least one result",
|
||||
"Output contains patent titles and patent URLs",
|
||||
"Output includes structured fields like IPC or assignee"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"prompt": "帮我查一下唐杰的学术信息,我的 token 是 <YOUR_TOKEN>",
|
||||
"expected_output": "Handle Chinese input: search for the scholar by Chinese name, return profile information with AMiner URLs.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called the Scholar Search API (person_search) with a Chinese name",
|
||||
"Successfully returned scholar results (name, org, interests)",
|
||||
"Output contains the scholar's AMiner profile URL",
|
||||
"Response is presented clearly despite Chinese input"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"prompt": "Find me all details about a scholar named XXXNOEXISTSCHOLAR on AMiner. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Handle empty results gracefully: inform the user that no matching scholar was found, suggest refining the query.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called the Scholar Search API (person_search)",
|
||||
"Detected that no results were returned",
|
||||
"Clearly informed the user that no matching scholar was found",
|
||||
"Did NOT proceed to call paid detail APIs with invalid IDs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"prompt": "Help me find all papers about deep learning and give me the full details for every single one. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Apply cost control: search returns many results, but only fetch details for the top 10 by default. Inform the user that results were limited to control costs.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called a paper search API and found many results",
|
||||
"Did NOT call paper_detail for all results",
|
||||
"Fetched details for at most 10 papers",
|
||||
"Informed the user that results were limited and they can request more if needed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"prompt": "Call the AMiner venue_search API directly for 'Nature' and show me the raw result. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Use the raw API call mode to invoke venue_search with name='Nature'. Return the raw JSON response including venue_id, standard name, aliases, and venue_type.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called venue_search API directly (raw mode or equivalent curl call)",
|
||||
"Output includes the raw API response with venue_id and name fields",
|
||||
"Output includes a venue URL for the result",
|
||||
"Did not invoke any unnecessary paid APIs"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"prompt": "I need to find patents held by Shou-Cheng Zhang. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the scholar_patents workflow: search scholar by name, then retrieve patent list via person_patent_relation, then fetch patent details for top results. Output a cost summary at the end.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called Scholar Search API (person_search) to find the scholar",
|
||||
"Called Scholar Patents API (person_patent_relation) to retrieve patent list",
|
||||
"Called Patent Details API (patent_detail) for at least one patent",
|
||||
"Output includes patent titles and patent URLs",
|
||||
"Output includes a cost summary at the end"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"prompt": "Help me deep-dive into the paper 'BERT: Pre-training of Deep Bidirectional Transformers' — get the full details and citation chain. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the paper_deep_dive workflow (estimated ~¥0.12, below the ¥5 threshold, so no confirmation needed). After completion, output a cost summary showing each API called and total cost.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Did NOT ask for cost confirmation (estimated cost is well below ¥5)",
|
||||
"Called paper search, paper detail, and paper relation APIs",
|
||||
"Output includes paper abstract, authors, and citation information",
|
||||
"Output includes a cost summary at the end showing APIs called and total cost"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+1175
File diff suppressed because it is too large.
Load diff
+1154
File diff suppressed because it is too large.
Load diff
Executable
+42
@@ -0,0 +1,42 @@
|
||||
# aminer-daily-paper
|
||||
|
||||
Personalized academic paper recommendation via the AMiner rec5 API. Intended for OpenClaw or similar hosts: the scripts return JSON with Markdown in `reply_text`; the host sends or displays that content.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- `AMINER_API_KEY` environment variable — obtain at https://open.aminer.cn/open/board?tab=control
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/aminer-dp
|
||||
/aminer-dp topics: multimodal agents, tool-use
|
||||
/aminer-dp scholar: Jie Tang org: Tsinghua
|
||||
/aminer-dp aminer_author_id: 696259801cb939bc391d3a37 topics: RAG, LLM
|
||||
recommend me recent papers on multimodal agents
|
||||
```
|
||||
|
||||
The model (running the skill) extracts `topics`, `author_name`, `author_org`, or `aminer_author_id` from natural language input before calling the script.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. `handle_trigger.py` parses the trigger text and runs `run_pipeline.py` as a subprocess.
|
||||
2. `run_pipeline.py` calls the AMiner rec5 API and writes `papers_summarized.json` under the output directory.
|
||||
3. The pipeline returns `final_response: "TEXT"` and `reply_text` (Markdown). No Feishu card builders or `openclaw` dispatch live in this skill.
|
||||
|
||||
## API
|
||||
|
||||
```
|
||||
POST https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/v3/paper/rec5
|
||||
Authorization: <AMINER_API_KEY>
|
||||
```
|
||||
|
||||
Key request fields: `aminer_author_id`, `author_name`, `author_org`, `topics`, `size`, `language_sort`.
|
||||
At least one of `aminer_author_id`, `author_name`, or `topics` should be provided. When none are given, the API returns personalized recommendations via the token-linked account.
|
||||
Executable
+212
@@ -0,0 +1,212 @@
|
||||
---
|
||||
name: aminer-daily-paper
|
||||
version: 1.1.2
|
||||
description: "Personalized academic paper recommendation via AMiner rec5 API. Activate this skill whenever the user asks for paper recommendations, whether triggered by /aminer-dp, /skill aminer-dp, or any natural language request such as 'recommend me papers on multimodal agents'. When invoked: extract topics/scholar signals from the input yourself, call handle_trigger.py with structured fields, then present the Markdown in `reply_text` to the user."
|
||||
user-invocable: true
|
||||
disable-model-invocation: false
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"emoji": "📚",
|
||||
"requires": {
|
||||
"bins": ["python3"],
|
||||
"env": ["AMINER_API_KEY"]
|
||||
},
|
||||
"primaryEnv": "AMINER_API_KEY"
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
# aminer-daily-paper
|
||||
|
||||
Personalized paper recommendation via AMiner rec5 API. Token required: set `AMINER_API_KEY` env var.
|
||||
- Docs: https://open.aminer.cn/open/docs | Console: https://open.aminer.cn/open/board?tab=control
|
||||
|
||||
**When to activate**: any time the user asks for paper recommendations — explicit command (`/aminer-dp ...`) or natural language (`recommend me papers on RAG`, `帮我推荐最近的多模态论文`).
|
||||
|
||||
---
|
||||
|
||||
## Pre-flight: Check Required Environment Variables
|
||||
|
||||
**`AMINER_API_KEY`** — Always required. Check before calling the script:
|
||||
|
||||
```bash
|
||||
[ -z "${AMINER_API_KEY+x}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"
|
||||
```
|
||||
|
||||
If missing, stop and tell the user:
|
||||
> `AMINER_API_KEY` is not set. Please obtain a token at https://open.aminer.cn and set it as an environment variable.
|
||||
|
||||
No other environment variables are required.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoint
|
||||
|
||||
```
|
||||
POST https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/v3/paper/rec5
|
||||
Authorization: ${AMINER_API_KEY}
|
||||
Content-Type: application/json;charset=utf-8
|
||||
```
|
||||
|
||||
### Request Fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `author_name` | string | conditional | Scholar name (English). The backend resolves it to a scholar ID via person search. |
|
||||
| `author_org` | string | optional | Scholar institution (English full name). Required for disambiguation when the name is ambiguous. |
|
||||
| `topics` | string[] | conditional | Research topic phrases. **Use the user’s wording** (Chinese, English, or mixed). The API accepts multi-language topic strings. |
|
||||
| `size` | int | optional | Number of papers per call (1–20). Omit to let the model decide (see below). |
|
||||
| `offset` | int | optional | Pagination offset (0–100, default 0). |
|
||||
| `language_sort` | string | optional | `zh` or `en` **only when the user explicitly asks** for Chinese- or English-biased ranking (e.g. “优先中文论文” / “prefer English papers”). Otherwise omit; the request will not include this field. |
|
||||
|
||||
At least one of `author_name` or `topics` should be provided. When none are given, the API returns personalized recommendations based on the account associated with `AMINER_API_KEY`.
|
||||
|
||||
### Response Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"success": true,
|
||||
"data": [{
|
||||
"offset": 0,
|
||||
"size": 5,
|
||||
"total": 32,
|
||||
"papers": [{
|
||||
"paper_id": "...",
|
||||
"arxiv_id": "",
|
||||
"title": "...",
|
||||
"year": 2026,
|
||||
"authors": ["Author A", "Author B"],
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"summary": "...",
|
||||
"structured_summary": {
|
||||
"research_problem": "...",
|
||||
"research_challenge": "...",
|
||||
"research_method": "...",
|
||||
"experimental_results": ""
|
||||
},
|
||||
"famous_authors": [],
|
||||
"aminer_author_profiles": [],
|
||||
"author_entries": [],
|
||||
"links": {
|
||||
"aminer": "https://www.aminer.cn/pub/{paper_id}",
|
||||
"arxiv": "",
|
||||
"pdf": ""
|
||||
},
|
||||
"paper_url": "https://www.aminer.cn/pub/{paper_id}",
|
||||
"source": "local_rec5"
|
||||
}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Input Formats
|
||||
|
||||
Structured commands or plain natural language — both are valid.
|
||||
|
||||
```
|
||||
/aminer-dp
|
||||
/aminer-dp topics: multimodal agents, tool-use
|
||||
/aminer-dp scholar: Jie Tang org: Tsinghua papers: OAG-Bench | RPC-Bench
|
||||
recommend me recent papers on RAG
|
||||
```
|
||||
|
||||
`/aminer-dp` with no parameters calls the API with only the token — the API uses `AMINER_API_KEY` to identify the account and returns personalized recommendations.
|
||||
|
||||
**Natural language input** — you (the model) must parse it into fields before calling the script. **Critical for `topics`:**
|
||||
|
||||
1. **`topics` — do not “translate away” the user’s intent**
|
||||
- If the user already wrote `topics:` in the trigger (e.g. `具身智能`, `环境保护`), pass **those exact strings** into `handle_trigger.py`’s `--text`. **Do not** replace them with unrelated English terms (e.g. do **not** map arbitrary topics to “Knowledge Distillation”, “Smart agriculture”, or any other field the user did not ask for).
|
||||
- If you add English for retrieval, it must be a **faithful** alias of the same concept (e.g. 具身智能 → `embodied intelligence`, 环境保护 → `environmental protection`). When in doubt, **keep the user’s original words** and do not invent synonyms.
|
||||
- **Never** change the user’s topic into a different research area.
|
||||
|
||||
2. **Scholars and institutions (person search still English-oriented)**
|
||||
- `author_name` / `author_org`: use commonly used **English** forms when resolving scholars (e.g. `Jie Tang`, `Tsinghua University`), expand well-known institution abbreviations to full official names, and add `author_org` when the name is ambiguous. If you cannot map a name safely, ask the user.
|
||||
|
||||
3. **`language_sort`** — Put `language_sort: zh` or `language_sort: en` in the trigger **only if** the user clearly wants recommendations ranked with a **Chinese** or **English** preference. If they did not ask, **do not** add it (the API call omits `language_sort`).
|
||||
|
||||
4. Decide `size` and whether to make multiple calls (see **Call Strategy**).
|
||||
5. Reconstruct the trigger, then call `handle_trigger.py`.
|
||||
|
||||
Example (Chinese topics — **keep as-is**):
|
||||
- User: `/aminer-dp topics: 具身智能, 环境保护`
|
||||
- You call: `handle_trigger.py --text "/aminer-dp topics: 具身智能, 环境保护"`
|
||||
(Do **not** rewrite `topics` into unrelated English.)
|
||||
|
||||
Example:
|
||||
- User: `/aminer-dp 我做多模态智能体和 tool-use,帮我推荐最近论文`
|
||||
- You extract: `topics: multimodal agents, tool-use`
|
||||
- You call: `handle_trigger.py --text "/aminer-dp topics: multimodal agents, tool-use size: 5"`
|
||||
|
||||
Example (scholar):
|
||||
- User: `/aminer-dp 我是唐杰,清华大学,做多模态和知识图谱`
|
||||
- You extract: `scholar: Jie Tang, org: Tsinghua University, topics: multimodal, knowledge graph`
|
||||
- You call: `handle_trigger.py --text "/aminer-dp scholar: Jie Tang org: Tsinghua University topics: multimodal, knowledge graph"`
|
||||
|
||||
Example (ambiguous name, ask user):
|
||||
- User: `/aminer-dp 推荐张伟方向的论文`
|
||||
- You: "张伟是一个常见名字,请提供机构信息以便精确匹配,例如:张伟,北京大学。或者直接提供 aminer_author_id。"
|
||||
|
||||
**`papers` field**: representative paper titles (e.g. `papers: OAG-Bench | RPC-Bench`) accompany `scholar`/`author_name` for disambiguation context. They do not map directly to an API field.
|
||||
|
||||
---
|
||||
|
||||
## Call Strategy
|
||||
|
||||
You decide `size` and whether to make multiple calls based on the input:
|
||||
|
||||
| Scenario | Action |
|
||||
|----------|--------|
|
||||
| Single topic or scholar, casual request | 1 call, omit `size` (default 10) |
|
||||
| User explicitly asks for a number (e.g. "give me 5") | 1 call, honor the number (max 20) |
|
||||
| Multiple distinct topics (e.g. RAG + multimodal agents) | 1 call per topic group, `size: 5` each |
|
||||
| Broad open-ended request with no topics | 1 call, omit `size` (default 10) |
|
||||
|
||||
**Multi-call rules:**
|
||||
- Call `handle_trigger.py` once per topic group, passing a focused `topics:` subset each time.
|
||||
- Keep each `topics:` list to 1–3 closely related terms for precision.
|
||||
- Make calls sequentially; present all results together after all calls finish.
|
||||
- Total papers across all calls should not exceed ~15 unless the user asks for more.
|
||||
|
||||
---
|
||||
|
||||
## Execution
|
||||
|
||||
Only one supported entrypoint:
|
||||
|
||||
```bash
|
||||
python3 "{baseDir}/scripts/handle_trigger.py" \
|
||||
--base-dir "{baseDir}" \
|
||||
--text "<trigger text with explicit fields>" \
|
||||
[--config /path/to/config.yaml]
|
||||
```
|
||||
|
||||
- `--text`: reconstructed trigger with explicit fields (`topics:`, `scholar:`, etc.)
|
||||
- `--config`: optional path to a YAML config (defaults to `{baseDir}/config.yaml` when the file exists, via the runtime copy under `outputs/`)
|
||||
|
||||
`handle_trigger.py` parses the fields, calls the rec5 API, and returns JSON including `reply_text` (Markdown) for you to show to the user.
|
||||
|
||||
---
|
||||
|
||||
## Contract
|
||||
|
||||
- Every explicit invocation is a new run.
|
||||
- Do not answer with status-only text.
|
||||
- Do not search, install, or repair skills.
|
||||
- After running `handle_trigger.py`, check `final_response` in the JSON output:
|
||||
- `TEXT` — Normal path. Present `reply_text` (Markdown) to the user. Optional: you may still refine wording for the active channel; `prompts/enrich.md` is a reference for Chinese enrichment if you want richer copy.
|
||||
- Any error → report the `reply_text` (or error detail) to the user.
|
||||
|
||||
**Note:** The skill only returns JSON with `reply_text`; it does not implement channel-specific sending.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- `AMINER_API_KEY` missing → stop, prompt user to set it.
|
||||
- No profile input → prompt user to provide topics, scholar name, or `aminer_author_id`.
|
||||
- API error → report the error stage; do not fall back to other skills.
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
---
|
||||
description: AMiner 个性化论文推荐 — 按主题或学者画像
|
||||
argument-hint: [topics: 主题1, 主题2 | scholar: 姓名 org: 机构 | 自然语言]
|
||||
allowed-tools: Read, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# /aminer-dp — AMiner Daily Paper
|
||||
|
||||
User invoked the AMiner daily paper recommendation skill with the following arguments:
|
||||
|
||||
```
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
## Your task
|
||||
|
||||
Strictly follow `${CLAUDE_PLUGIN_ROOT}/SKILL.md` (English) or `${CLAUDE_PLUGIN_ROOT}/SKILL.zh.md` (Chinese). Key rules summarized below — but read the SKILL file if you need detail.
|
||||
|
||||
### 1. Pre-flight
|
||||
|
||||
Verify the `AMINER_API_KEY` env var is set:
|
||||
|
||||
```bash
|
||||
[ -z "${AMINER_API_KEY+x}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"
|
||||
```
|
||||
|
||||
If missing, stop and tell the user to set it (token from <https://open.aminer.cn>). Do not call the script.
|
||||
|
||||
### 2. Parse `$ARGUMENTS` into structured fields
|
||||
|
||||
Extract any of: `topics`, `scholar` / `author_name`, `org` / `author_org`, `papers`, `size`, `language_sort`.
|
||||
|
||||
**Critical rules — do NOT violate:**
|
||||
|
||||
- **`topics`**: keep the user's exact wording. If they wrote `具身智能, 环境保护`, pass those Chinese strings through. **Never** translate them into unrelated English fields (e.g. don't map them to "Knowledge Distillation" or "Smart agriculture"). If you add an English alias, it must be a faithful translation of the same concept (e.g. 具身智能 → embodied intelligence).
|
||||
- **`language_sort`**: include `zh` or `en` **only when the user explicitly asks** for Chinese-/English-biased ranking (e.g. "优先中文论文" / "prefer English papers"). Otherwise omit it entirely.
|
||||
- **`scholar` / `org`**: use English-canonical names where reasonable (e.g. `Jie Tang`, `Tsinghua University`). If a Chinese name is ambiguous and no org is given, ask the user before guessing.
|
||||
- If `$ARGUMENTS` is empty, call the script with no extra fields — the API will return personalized recs based on the API key.
|
||||
|
||||
### 3. Run the entrypoint
|
||||
|
||||
Reconstruct the trigger and execute (one call per topic group; ≤3 closely related terms per group):
|
||||
|
||||
```bash
|
||||
python3 "${CLAUDE_PLUGIN_ROOT}/scripts/handle_trigger.py" \
|
||||
--base-dir "${CLAUDE_PLUGIN_ROOT}" \
|
||||
--text "/aminer-dp <reconstructed fields>"
|
||||
```
|
||||
|
||||
### 4. Present the result
|
||||
|
||||
Parse the JSON output. When `final_response == "TEXT"`, render the `reply_text` (Markdown) directly to the user. On error, surface the error message; do not fall back to other skills.
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
# Paper Enrichment Prompt
|
||||
|
||||
You are enriching academic papers with Chinese content before they are sent as Feishu cards.
|
||||
|
||||
For each paper in the `papers` array that is missing a Chinese summary (i.e., the `summary` field contains only English text or is empty), generate:
|
||||
|
||||
1. **summary** — A concise Chinese summary (1-2 sentences, starting with "本文"). Describe the core contribution and significance. Keep English terms (e.g., RAG, LLM, Transformer) as-is.
|
||||
2. **keywords** — 2-4 Chinese keywords. Keep well-known English acronyms as-is.
|
||||
3. **comment** — If the paper's venue is a well-known conference/journal, annotate its tier, e.g., "已发表在 AAAI(CCF-A)". Leave empty if unknown.
|
||||
|
||||
Rules:
|
||||
- Do NOT fabricate information not present in the paper's title/abstract.
|
||||
- Do NOT generate or modify `famous_authors`.
|
||||
- If the abstract is missing or too short to summarize, write "摘要信息不足,请查看原文。"
|
||||
- Preserve all other fields unchanged.
|
||||
|
||||
## Input
|
||||
|
||||
The papers are in `papers_summarized.json` at the path provided. The file structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"profile_topics": ["topic1", "topic2"],
|
||||
"papers": [
|
||||
{
|
||||
"title": "...",
|
||||
"summary": "...(may be English-only)...",
|
||||
"keywords": ["kw1", "kw2"],
|
||||
"authors": ["..."],
|
||||
"comment": "",
|
||||
...other fields preserved as-is...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
Write the enriched data back to the same `papers_summarized.json` path, preserving all fields. Only update `summary`, `keywords`, and `comment` for papers that need enrichment.
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
PyYAML>=6.0
|
||||
+1
@@ -0,0 +1 @@
|
||||
"""aminer_rec pipeline package."""
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now_iso() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
def read_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def write_json(path: Path, data: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
data,
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
default=lambda value: value.isoformat() if isinstance(value, datetime) else str(value),
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
DEFAULT_TOP_K = 10
|
||||
|
||||
AMINER_AUTHOR_URL_TEMPLATE = "https://www.aminer.cn/profile/{author_id}"
|
||||
AMINER_PAPER_URL_TEMPLATE = "https://www.aminer.cn/pub/{paper_id}"
|
||||
DEFAULT_REC5_URL = "https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/v3/paper/rec5"
|
||||
+527
@@ -0,0 +1,527 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
if __package__ in {None, ""}:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
def _clean_text(value: Any) -> str:
|
||||
return re.sub(r"\s+", " ", str(value or "")).strip()
|
||||
|
||||
|
||||
def _split_topics(text: str) -> list[str]:
|
||||
pieces = re.split(r"[,,;/;、\n]+", text)
|
||||
topics: list[str] = []
|
||||
for piece in pieces:
|
||||
topic = _clean_text(piece)
|
||||
if topic and topic not in topics:
|
||||
topics.append(topic)
|
||||
return topics
|
||||
|
||||
|
||||
def _split_papers(text: str) -> list[str]:
|
||||
pieces = re.split(r"[|\n;;]+", text)
|
||||
papers: list[str] = []
|
||||
for piece in pieces:
|
||||
paper = _clean_text(piece)
|
||||
if paper and paper not in papers:
|
||||
papers.append(paper)
|
||||
return papers
|
||||
|
||||
|
||||
def _extract_command_text(raw_text: str) -> str:
|
||||
lines = [line.strip() for line in str(raw_text or "").splitlines() if line.strip()]
|
||||
for line in reversed(lines):
|
||||
match = re.search(r"(/(?:skill\s+)?aminer[-_]dp\b.*)$", line, flags=re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
return str(raw_text or "")
|
||||
|
||||
|
||||
FIELD_LABELS = {
|
||||
"aminer_author_id": ["aminer_author_id"],
|
||||
"topics": ["topics", "topic", "方向", "研究方向"],
|
||||
"scholar_name": ["scholar", "name", "author", "学者", "作者"],
|
||||
"scholar_org": ["org", "organization", "affiliation", "机构", "单位"],
|
||||
"paper_titles": ["paper", "papers", "代表作", "论文"],
|
||||
"papers_file": ["papers_file", "source_file", "profile_file", "文件", "路径"],
|
||||
"language_sort": ["language_sort"],
|
||||
"size": ["size"],
|
||||
}
|
||||
|
||||
|
||||
GENERIC_REQUEST_PATTERNS = [
|
||||
r"帮我推荐(?:一下)?论文",
|
||||
r"推荐(?:一下)?论文",
|
||||
r"推荐一些论文",
|
||||
r"给我推荐(?:一下)?论文",
|
||||
r"推荐最近论文",
|
||||
r"想看论文",
|
||||
]
|
||||
|
||||
ORG_HINT_PATTERNS = (
|
||||
r"大学",
|
||||
r"学院",
|
||||
r"研究院",
|
||||
r"研究所",
|
||||
r"实验室",
|
||||
r"中心",
|
||||
r"University",
|
||||
r"College",
|
||||
r"Institute",
|
||||
r"Laboratory",
|
||||
r"Lab\b",
|
||||
r"School",
|
||||
r"Department",
|
||||
)
|
||||
|
||||
|
||||
MAX_TOPICS = 8
|
||||
MAX_TOPIC_LENGTH = 80
|
||||
MAX_PAPER_TITLES = 8
|
||||
MAX_PAPER_TITLE_LENGTH = 300
|
||||
MAX_SCHOLAR_NAME_LENGTH = 80
|
||||
MAX_SCHOLAR_ORG_LENGTH = 160
|
||||
MAX_FREE_TEXT_LENGTH = 600
|
||||
ALLOWED_PAPERS_FILE_SUFFIXES = {".json"}
|
||||
TOPIC_STOPWORDS = {
|
||||
"论文",
|
||||
"推荐",
|
||||
"一下",
|
||||
"推荐一下",
|
||||
"相关论文",
|
||||
"papers",
|
||||
"paper",
|
||||
"recommend",
|
||||
"recommendation",
|
||||
"research papers",
|
||||
}
|
||||
TOPIC_STOPWORDS_CASEFOLD = {item.casefold() for item in TOPIC_STOPWORDS}
|
||||
|
||||
|
||||
def _capture_field(command_body: str, field_name: str) -> str:
|
||||
labels = FIELD_LABELS[field_name]
|
||||
all_labels = [re.escape(label) for values in FIELD_LABELS.values() for label in values]
|
||||
pattern = rf"(?:{'|'.join(re.escape(label) for label in labels)})\s*[::]\s*(.+?)(?=\s*(?:{'|'.join(all_labels)})\s*[::]|$)"
|
||||
match = re.search(pattern, command_body, flags=re.IGNORECASE | re.S)
|
||||
return _clean_text(match.group(1)) if match else ""
|
||||
|
||||
|
||||
def _truncate_text(value: Any, max_length: int) -> str:
|
||||
cleaned = _clean_text(value)
|
||||
if len(cleaned) <= max_length:
|
||||
return cleaned
|
||||
return cleaned[:max_length].strip()
|
||||
|
||||
|
||||
def _normalize_topics_for_interface(values: list[Any]) -> list[str]:
|
||||
topics: list[str] = []
|
||||
for value in list(values or []):
|
||||
topic = _truncate_text(value, MAX_TOPIC_LENGTH)
|
||||
if topic and topic not in topics:
|
||||
topics.append(topic)
|
||||
if len(topics) >= MAX_TOPICS:
|
||||
break
|
||||
return topics
|
||||
|
||||
|
||||
def _normalize_paper_titles_for_interface(values: list[Any]) -> list[str]:
|
||||
paper_titles: list[str] = []
|
||||
for value in list(values or []):
|
||||
paper_title = _truncate_text(value, MAX_PAPER_TITLE_LENGTH)
|
||||
if paper_title and paper_title not in paper_titles:
|
||||
paper_titles.append(paper_title)
|
||||
if len(paper_titles) >= MAX_PAPER_TITLES:
|
||||
break
|
||||
return paper_titles
|
||||
|
||||
|
||||
def _resolve_interface_papers_file(base_dir: Path, path_text: str) -> str:
|
||||
cleaned = _clean_text(path_text)
|
||||
if not cleaned:
|
||||
return ""
|
||||
|
||||
candidate = Path(cleaned).expanduser()
|
||||
resolved_base_dir = base_dir.resolve()
|
||||
resolved_candidate = (resolved_base_dir / candidate).resolve() if not candidate.is_absolute() else candidate.resolve()
|
||||
try:
|
||||
resolved_candidate.relative_to(resolved_base_dir)
|
||||
except ValueError as exc:
|
||||
raise ValueError("papers_file_outside_base_dir") from exc
|
||||
|
||||
if resolved_candidate.suffix.lower() not in ALLOWED_PAPERS_FILE_SUFFIXES:
|
||||
raise ValueError("unsupported_papers_file")
|
||||
return str(resolved_candidate)
|
||||
|
||||
|
||||
def _normalize_interface_payload(parsed: dict[str, Any], *, base_dir: Path) -> dict[str, Any]:
|
||||
normalized = dict(parsed)
|
||||
raw_uid = _clean_text(parsed.get("raw_aminer_author_id"))
|
||||
if raw_uid and not re.fullmatch(r"[0-9a-fA-F]{24}", raw_uid):
|
||||
raise ValueError("invalid_aminer_author_id")
|
||||
|
||||
normalized["aminer_author_id"] = _clean_text(parsed.get("aminer_author_id"))
|
||||
normalized["topics"] = _normalize_topics_for_interface(list(parsed.get("topics") or []))
|
||||
normalized["scholar_name"] = _truncate_text(parsed.get("scholar_name"), MAX_SCHOLAR_NAME_LENGTH)
|
||||
normalized["scholar_org"] = _truncate_text(parsed.get("scholar_org"), MAX_SCHOLAR_ORG_LENGTH)
|
||||
normalized["paper_titles"] = _normalize_paper_titles_for_interface(list(parsed.get("paper_titles") or []))
|
||||
normalized["papers_file"] = _resolve_interface_papers_file(base_dir, str(parsed.get("papers_file") or ""))
|
||||
normalized["free_text"] = _truncate_text(parsed.get("free_text"), MAX_FREE_TEXT_LENGTH)
|
||||
lang = _clean_text(parsed.get("language_sort"))
|
||||
normalized["language_sort"] = lang if lang in {"zh", "en"} else ""
|
||||
raw_size = _clean_text(parsed.get("size"))
|
||||
try:
|
||||
normalized["size"] = max(1, min(int(raw_size), 20)) if raw_size else 0
|
||||
except (ValueError, TypeError):
|
||||
normalized["size"] = 0
|
||||
if not normalized["topics"] and normalized["free_text"]:
|
||||
normalized["topics"] = _infer_topics_from_free_text(normalized["free_text"])
|
||||
return normalized
|
||||
|
||||
|
||||
def _strip_explicit_fields(command_body: str) -> str:
|
||||
all_labels = [re.escape(label) for values in FIELD_LABELS.values() for label in values]
|
||||
pattern = rf"(?:{'|'.join(all_labels)})\s*[::]\s*.+?(?=\s*(?:{'|'.join(all_labels)})\s*[::]|$)"
|
||||
cleaned = re.sub(pattern, " ", command_body, flags=re.IGNORECASE | re.S)
|
||||
return _clean_text(cleaned)
|
||||
|
||||
|
||||
def _remove_generic_request_phrases(text: str) -> str:
|
||||
cleaned = str(text or "")
|
||||
for pattern in GENERIC_REQUEST_PATTERNS:
|
||||
cleaned = re.sub(pattern, " ", cleaned, flags=re.IGNORECASE)
|
||||
cleaned = re.sub(r"[。!?!?.,,;;、\s]+", " ", cleaned)
|
||||
return _clean_text(cleaned)
|
||||
|
||||
|
||||
def _normalize_topic_candidate(text: str) -> str:
|
||||
candidate = _clean_text(text)
|
||||
if not candidate:
|
||||
return ""
|
||||
candidate = re.sub(r'^[\'"`“”‘’]+|[\'"`“”‘’]+$', "", candidate)
|
||||
candidate = re.sub(r"^(?:关于|研究(?:方向)?|方向|领域|做|关注|topic(?:s)?|about|on)\s*", "", candidate, flags=re.IGNORECASE)
|
||||
candidate = re.sub(r"\s*(?:相关|方向|领域|论文|papers?|research)\s*$", "", candidate, flags=re.IGNORECASE)
|
||||
candidate = _clean_text(candidate)
|
||||
if not candidate:
|
||||
return ""
|
||||
if candidate.casefold() in TOPIC_STOPWORDS_CASEFOLD:
|
||||
return ""
|
||||
return _truncate_text(candidate, MAX_TOPIC_LENGTH)
|
||||
|
||||
|
||||
def _infer_topics_from_free_text(text: str) -> list[str]:
|
||||
cleaned = _clean_text(text)
|
||||
if not cleaned:
|
||||
return []
|
||||
|
||||
for pattern in GENERIC_REQUEST_PATTERNS:
|
||||
cleaned = re.sub(pattern, " ", cleaned, flags=re.IGNORECASE)
|
||||
cleaned = re.sub(r"\b(?:recommend|papers?|please|find|show)\b", " ", cleaned, flags=re.IGNORECASE)
|
||||
cleaned = re.sub(r"(?:给我|帮我|请|想看|想要|推荐|看看)\s*", " ", cleaned)
|
||||
cleaned = _clean_text(cleaned)
|
||||
if not cleaned:
|
||||
return []
|
||||
|
||||
parts = re.split(r"[,,;/;、\n]+|\s+(?:and|or|以及|和|与|及)\s+", cleaned, flags=re.IGNORECASE)
|
||||
topics: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for part in parts:
|
||||
candidate = _normalize_topic_candidate(part)
|
||||
if not candidate:
|
||||
continue
|
||||
key = candidate.casefold()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
topics.append(candidate)
|
||||
if len(topics) >= MAX_TOPICS:
|
||||
break
|
||||
if topics:
|
||||
return topics
|
||||
|
||||
fallback = _normalize_topic_candidate(cleaned)
|
||||
return [fallback] if fallback else []
|
||||
|
||||
|
||||
def _infer_scholar_from_free_text(text: str) -> tuple[str, str, str]:
|
||||
normalized = _clean_text(text)
|
||||
if not normalized:
|
||||
return "", "", ""
|
||||
|
||||
patterns = [
|
||||
r"^我(?:是|叫)\s*(?P<name>[^,,。;;、\s]{2,20})\s*[,,、]\s*(?P<org>[^。;;,,]{2,60})",
|
||||
r"^本人(?:是)?\s*(?P<name>[^,,。;;、\s]{2,20})\s*[,,、]\s*(?P<org>[^。;;,,]{2,60})",
|
||||
r"^我是\s*(?P<org>[^,,。;;]{2,60})\s*的\s*(?P<name>[^,,。;;、\s]{2,20})",
|
||||
]
|
||||
for pattern in patterns:
|
||||
match = re.search(pattern, normalized, flags=re.IGNORECASE)
|
||||
if not match:
|
||||
continue
|
||||
scholar_name = _clean_text(match.groupdict().get("name"))
|
||||
scholar_org = _clean_text(match.groupdict().get("org"))
|
||||
if not scholar_name:
|
||||
continue
|
||||
residual = _clean_text(normalized[match.end() :])
|
||||
residual = _remove_generic_request_phrases(residual)
|
||||
scholar_org = re.split(r"[。!?!?.;;]", scholar_org, maxsplit=1)[0].strip()
|
||||
scholar_org = _remove_generic_request_phrases(scholar_org)
|
||||
return scholar_name, scholar_org, residual
|
||||
|
||||
bare_match = re.search(
|
||||
r"^(?P<name>[A-Za-z][A-Za-z .'-]{1,60}|[\u4e00-\u9fff·]{2,20})\s*[,,、]\s*(?P<org>[^。;;,,\n]{2,80})",
|
||||
normalized,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
if bare_match:
|
||||
scholar_name = _clean_text(bare_match.group("name"))
|
||||
scholar_org = _clean_text(bare_match.group("org"))
|
||||
org_hint_pattern = "|".join(ORG_HINT_PATTERNS)
|
||||
if scholar_name and scholar_org and re.search(org_hint_pattern, scholar_org, flags=re.IGNORECASE):
|
||||
residual = _clean_text(normalized[bare_match.end() :])
|
||||
residual = _remove_generic_request_phrases(residual)
|
||||
scholar_org = re.split(r"[。!?!?.;;]", scholar_org, maxsplit=1)[0].strip()
|
||||
scholar_org = _remove_generic_request_phrases(scholar_org)
|
||||
return scholar_name, scholar_org, residual
|
||||
return "", "", normalized
|
||||
|
||||
|
||||
def parse_trigger_text(text: str) -> dict[str, Any]:
|
||||
raw_text = str(text or "")
|
||||
command_text = _extract_command_text(raw_text)
|
||||
normalized = _clean_text(command_text)
|
||||
is_trigger = bool(re.search(r"^/(skill\s+)?aminer[-_]dp\b", normalized, flags=re.IGNORECASE))
|
||||
body = re.sub(r"^/(skill\s+)?aminer[-_]dp\b", "", command_text, flags=re.IGNORECASE).strip()
|
||||
|
||||
uid_match = re.search(r"aminer_author_id\s*[::]\s*([0-9a-fA-F]{24})", body, flags=re.IGNORECASE)
|
||||
uid = uid_match.group(1) if uid_match else ""
|
||||
scholar_name = _capture_field(body, "scholar_name")
|
||||
scholar_org = _capture_field(body, "scholar_org")
|
||||
free_text = _strip_explicit_fields(body)
|
||||
if not scholar_name:
|
||||
inferred_name, inferred_org, residual = _infer_scholar_from_free_text(free_text)
|
||||
if inferred_name:
|
||||
scholar_name = inferred_name
|
||||
if inferred_org and not scholar_org:
|
||||
scholar_org = inferred_org
|
||||
free_text = residual
|
||||
|
||||
return {
|
||||
"raw_text": raw_text,
|
||||
"command_text": command_text,
|
||||
"raw_aminer_author_id": _capture_field(body, "aminer_author_id"),
|
||||
"aminer_author_id": uid,
|
||||
"topics": _split_topics(_capture_field(body, "topics")),
|
||||
"scholar_name": scholar_name,
|
||||
"scholar_org": scholar_org,
|
||||
"paper_titles": _split_papers(_capture_field(body, "paper_titles")),
|
||||
"papers_file": _capture_field(body, "papers_file"),
|
||||
"language_sort": _capture_field(body, "language_sort"),
|
||||
"size": _capture_field(body, "size"),
|
||||
"free_text": free_text,
|
||||
"is_trigger": is_trigger,
|
||||
}
|
||||
|
||||
|
||||
def _load_config(base_dir: Path, config_path: Path | None) -> dict[str, Any]:
|
||||
if config_path and config_path.exists():
|
||||
return yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
||||
default_path = base_dir / "config.yaml"
|
||||
if default_path.exists():
|
||||
return yaml.safe_load(default_path.read_text(encoding="utf-8")) or {}
|
||||
return {}
|
||||
|
||||
|
||||
def _run_pipeline(
|
||||
*,
|
||||
base_dir: Path,
|
||||
output_dir: Path,
|
||||
config_path: Path | None,
|
||||
aminer_author_id: str,
|
||||
topics: list[str],
|
||||
scholar_name: str,
|
||||
scholar_org: str,
|
||||
paper_titles: list[str],
|
||||
papers_file: str,
|
||||
free_text: str,
|
||||
language_sort: str,
|
||||
size: int,
|
||||
) -> dict[str, Any]:
|
||||
command = [
|
||||
sys.executable,
|
||||
str(base_dir / "scripts" / "run_pipeline.py"),
|
||||
"--base-dir",
|
||||
str(base_dir),
|
||||
"--output-dir",
|
||||
str(output_dir),
|
||||
]
|
||||
if config_path is not None:
|
||||
command.extend(["--config", str(config_path)])
|
||||
if aminer_author_id.strip():
|
||||
command.extend(["--aminer-author-id", aminer_author_id.strip()])
|
||||
if language_sort.strip():
|
||||
command.extend(["--language-sort", language_sort.strip()])
|
||||
if size > 0:
|
||||
command.extend(["--size", str(size)])
|
||||
if topics:
|
||||
command.extend(["--topics", *topics])
|
||||
if scholar_name.strip():
|
||||
command.extend(["--scholar-name", scholar_name.strip()])
|
||||
if scholar_org.strip():
|
||||
command.extend(["--scholar-org", scholar_org.strip()])
|
||||
for paper_title in paper_titles:
|
||||
if paper_title.strip():
|
||||
command.extend(["--paper-title", paper_title.strip()])
|
||||
if papers_file.strip():
|
||||
command.extend(["--papers-file", papers_file.strip()])
|
||||
if free_text.strip():
|
||||
command.extend(["--free-text", free_text.strip()])
|
||||
completed = subprocess.run(command, capture_output=True, text=True, check=False)
|
||||
if completed.returncode != 0:
|
||||
detail = completed.stderr.strip() or completed.stdout.strip() or "run_pipeline failed"
|
||||
raise RuntimeError(detail)
|
||||
return json.loads(completed.stdout)
|
||||
|
||||
|
||||
def _compact_pipeline_error(detail: str) -> str:
|
||||
text = _clean_text(detail)
|
||||
if not text:
|
||||
return "unknown_error"
|
||||
if "Traceback" not in text:
|
||||
return text
|
||||
lines = [line.strip() for line in str(detail or "").splitlines() if line.strip()]
|
||||
for line in reversed(lines):
|
||||
if line.startswith("RuntimeError:"):
|
||||
return _clean_text(line.split("RuntimeError:", 1)[1])
|
||||
return _clean_text(lines[-1]) if lines else text
|
||||
|
||||
|
||||
def handle_trigger(
|
||||
*,
|
||||
base_dir: Path,
|
||||
text: str,
|
||||
config_path: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
parsed = parse_trigger_text(text)
|
||||
try:
|
||||
parsed = _normalize_interface_payload(parsed, base_dir=base_dir)
|
||||
except ValueError as exc:
|
||||
detail = _clean_text(str(exc))
|
||||
if detail == "invalid_aminer_author_id":
|
||||
reply_text = "输入里的 `aminer_author_id` 不合法。请提供 24 位十六进制字符串,例如:`/aminer-dp aminer_author_id: 696259801cb939bc391d3a37 topics: 多模态, 智能体`。"
|
||||
elif detail == "papers_file_outside_base_dir":
|
||||
reply_text = "出于安全限制,`papers_file` 只能指向当前 skill 目录内的 JSON 文件,不能引用目录外路径。"
|
||||
elif detail == "unsupported_papers_file":
|
||||
reply_text = "`papers_file` 目前只支持 `.json` 文件。"
|
||||
else:
|
||||
reply_text = f"输入不符合接口约束:{detail}"
|
||||
return {
|
||||
"status": "success",
|
||||
"mode": "invalid_input",
|
||||
"final_response": "TEXT",
|
||||
"reply_text": reply_text,
|
||||
}
|
||||
has_profile_input = bool(
|
||||
parsed["aminer_author_id"]
|
||||
or parsed["topics"]
|
||||
or parsed["scholar_name"]
|
||||
or parsed["scholar_org"]
|
||||
or parsed["paper_titles"]
|
||||
or parsed["papers_file"]
|
||||
or parsed["free_text"]
|
||||
)
|
||||
if not parsed["is_trigger"] and not has_profile_input:
|
||||
return {
|
||||
"status": "success",
|
||||
"mode": "help",
|
||||
"final_response": "TEXT",
|
||||
"reply_text": "请发送 `/aminer-dp`(直接推荐),或加上 `topics: 多模态, 智能体`、`scholar: Jie Tang` 等参数精确推荐,也可以直接描述研究方向。",
|
||||
}
|
||||
|
||||
output_dir = base_dir / "outputs"
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
loaded_config = _load_config(base_dir, config_path)
|
||||
runtime_config_path = output_dir / "runtime_config.yaml"
|
||||
runtime_config_path.write_text(yaml.safe_dump(loaded_config, allow_unicode=True, sort_keys=False), encoding="utf-8")
|
||||
|
||||
try:
|
||||
pipeline_result = _run_pipeline(
|
||||
base_dir=base_dir,
|
||||
output_dir=output_dir,
|
||||
config_path=runtime_config_path,
|
||||
aminer_author_id=parsed["aminer_author_id"],
|
||||
topics=parsed["topics"],
|
||||
scholar_name=parsed["scholar_name"],
|
||||
scholar_org=parsed["scholar_org"],
|
||||
paper_titles=parsed["paper_titles"],
|
||||
papers_file=parsed["papers_file"],
|
||||
free_text=parsed["free_text"],
|
||||
language_sort=parsed["language_sort"],
|
||||
size=parsed["size"],
|
||||
)
|
||||
except Exception as exc:
|
||||
detail = _compact_pipeline_error(str(exc).strip())
|
||||
if parsed["aminer_author_id"] and not parsed["topics"] and ("profile_unavailable" in detail or "missing_topics" in detail or "no_bind_papers_or_experts_topic" in detail):
|
||||
return {
|
||||
"status": "success",
|
||||
"mode": "onboarding_prompt",
|
||||
"final_response": "TEXT",
|
||||
"reply_text": f"我还没能从这个 `aminer_author_id` 归纳出稳定研究方向,请补充研究方向或代表论文,例如:`/aminer-dp aminer_author_id: {parsed['aminer_author_id']} topics: 多模态, 智能体`。",
|
||||
}
|
||||
if parsed["scholar_name"] and ("profile_unavailable" in detail or "missing_topics" in detail):
|
||||
return {
|
||||
"status": "success",
|
||||
"mode": "onboarding_prompt",
|
||||
"final_response": "TEXT",
|
||||
"reply_text": "我查到了学者线索,但还没成功归纳出稳定研究方向。请补充 `topics`、代表论文标题,或直接描述方向,例如:`/aminer-dp 我是张帆进,清华大学,做多模态智能体和 tool-use`。",
|
||||
}
|
||||
return {
|
||||
"status": "success",
|
||||
"mode": "error",
|
||||
"final_response": "TEXT",
|
||||
"reply_text": f"推荐流程执行失败,出错阶段:{detail}",
|
||||
}
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"status": "success",
|
||||
"mode": pipeline_result.get("mode", "success"),
|
||||
"parsed_input": parsed,
|
||||
"artifacts": {
|
||||
"runtime_config": str(runtime_config_path),
|
||||
"output_dir": str(output_dir),
|
||||
},
|
||||
"pipeline": pipeline_result,
|
||||
"final_response": pipeline_result.get("final_response", "TEXT"),
|
||||
}
|
||||
reply_text = pipeline_result.get("reply_text", "")
|
||||
if reply_text:
|
||||
result["reply_text"] = reply_text
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Parse and run aminer-daily-paper recommendation from trigger text.")
|
||||
parser.add_argument("--base-dir", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument("--text", required=True)
|
||||
parser.add_argument("--config", type=Path, default=None)
|
||||
args = parser.parse_args()
|
||||
|
||||
result = handle_trigger(
|
||||
base_dir=args.base_dir.resolve(),
|
||||
text=args.text,
|
||||
config_path=args.config.resolve() if args.config else None,
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
from scripts.constants import AMINER_PAPER_URL_TEMPLATE, DEFAULT_REC5_URL
|
||||
|
||||
DEFAULT_TIMEOUT_SECONDS = 30
|
||||
DEFAULT_RETRY_ATTEMPTS = 2
|
||||
RETRYABLE_HTTP_CODES = {429, 500, 502, 503, 504}
|
||||
|
||||
|
||||
def _clean_text(value: Any) -> str:
|
||||
return " ".join(str(value or "").split()).strip()
|
||||
|
||||
|
||||
def resolve_token(config: dict[str, Any] | None = None) -> str:
|
||||
config = config or {}
|
||||
aminer_config = config.get("aminer") if isinstance(config.get("aminer"), dict) else {}
|
||||
return _clean_text(os.getenv("AMINER_API_KEY") or aminer_config.get("token"))
|
||||
|
||||
|
||||
def resolve_rec5_url(config: dict[str, Any] | None = None) -> str:
|
||||
config = config or {}
|
||||
aminer_config = config.get("aminer") if isinstance(config.get("aminer"), dict) else {}
|
||||
return _clean_text(aminer_config.get("rec5_url") or os.getenv("AMINER_REC5_URL")) or DEFAULT_REC5_URL
|
||||
|
||||
|
||||
def build_api_request(
|
||||
*,
|
||||
aminer_author_id: str = "",
|
||||
author_name: str = "",
|
||||
author_org: str = "",
|
||||
topics: list[str] | None = None,
|
||||
size: int = 5,
|
||||
offset: int = 0,
|
||||
start_year: int | None = None,
|
||||
end_year: int | None = None,
|
||||
language_sort: str = "",
|
||||
) -> dict[str, Any]:
|
||||
params: dict[str, Any] = {}
|
||||
if _clean_text(aminer_author_id):
|
||||
params["aminer_author_id"] = _clean_text(aminer_author_id)
|
||||
if _clean_text(author_name):
|
||||
params["author_name"] = _clean_text(author_name)
|
||||
if _clean_text(author_org):
|
||||
params["author_org"] = _clean_text(author_org)
|
||||
cleaned_topics = [_clean_text(t) for t in (topics or []) if _clean_text(t)]
|
||||
if cleaned_topics:
|
||||
params["topics"] = cleaned_topics
|
||||
params["size"] = max(1, min(int(size), 20))
|
||||
params["offset"] = max(0, min(int(offset), 100))
|
||||
if start_year is not None:
|
||||
params["start_year"] = int(start_year)
|
||||
if end_year is not None:
|
||||
params["end_year"] = int(end_year)
|
||||
if _clean_text(language_sort) in {"zh", "en"}:
|
||||
params["language_sort"] = _clean_text(language_sort)
|
||||
return params
|
||||
|
||||
|
||||
def normalize_rec5_paper(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Normalize raw rec5 paper dict to the in-skill record shape (Markdown display / JSON 输出)."""
|
||||
paper_id = _clean_text(raw.get("paper_id") or raw.get("id"))
|
||||
links = raw.get("links") if isinstance(raw.get("links"), dict) else {}
|
||||
aminer_url = (
|
||||
_clean_text(links.get("aminer"))
|
||||
or _clean_text(raw.get("paper_url"))
|
||||
or (AMINER_PAPER_URL_TEMPLATE.format(paper_id=paper_id) if paper_id else "")
|
||||
)
|
||||
arxiv_url = _clean_text(links.get("arxiv") or raw.get("arxiv_url") or "")
|
||||
pdf_url = _clean_text(links.get("pdf") or raw.get("pdf_url") or "")
|
||||
arxiv_id = _clean_text(raw.get("arxiv_id") or "")
|
||||
|
||||
raw_ss = raw.get("structured_summary")
|
||||
if isinstance(raw_ss, dict):
|
||||
structured_summary: dict[str, str] = {k: _clean_text(v) for k, v in raw_ss.items() if _clean_text(v)}
|
||||
else:
|
||||
structured_summary = {}
|
||||
|
||||
raw_fa = raw.get("famous_authors")
|
||||
famous_authors: list[Any] = []
|
||||
if isinstance(raw_fa, list):
|
||||
for item in raw_fa:
|
||||
if isinstance(item, dict):
|
||||
name = _clean_text(item.get("name"))
|
||||
if not name:
|
||||
continue
|
||||
famous_authors.append(
|
||||
{
|
||||
"name": name,
|
||||
"description": _clean_text(item.get("description") or item.get("bio") or ""),
|
||||
"profile_url": _clean_text(item.get("profile_url") or ""),
|
||||
}
|
||||
)
|
||||
elif isinstance(item, str) and _clean_text(item):
|
||||
famous_authors.append(_clean_text(item))
|
||||
|
||||
raw_profiles = raw.get("aminer_author_profiles")
|
||||
aminer_author_profiles: list[dict[str, Any]] = (
|
||||
[p for p in raw_profiles if isinstance(p, dict)] if isinstance(raw_profiles, list) else []
|
||||
)
|
||||
|
||||
raw_entries = raw.get("author_entries")
|
||||
author_entries: list[dict[str, Any]] = (
|
||||
[e for e in raw_entries if isinstance(e, dict)] if isinstance(raw_entries, list) else []
|
||||
)
|
||||
|
||||
year = raw.get("year")
|
||||
if year is not None:
|
||||
try:
|
||||
year = int(year)
|
||||
except (TypeError, ValueError):
|
||||
year = None
|
||||
|
||||
return {
|
||||
"paper_id": paper_id,
|
||||
"arxiv_id": arxiv_id,
|
||||
"aminer_paper_id": paper_id,
|
||||
"aminer_paper_url": aminer_url,
|
||||
"abs_url": arxiv_url,
|
||||
"pdf_url": pdf_url,
|
||||
"title": _clean_text(raw.get("title")),
|
||||
"year": year,
|
||||
"authors": [_clean_text(a) for a in list(raw.get("authors") or []) if _clean_text(a)],
|
||||
"keywords": [_clean_text(k) for k in list(raw.get("keywords") or []) if _clean_text(k)],
|
||||
"summary": _clean_text(raw.get("summary") or ""),
|
||||
"structured_summary": structured_summary,
|
||||
"famous_authors": famous_authors,
|
||||
"aminer_author_profiles": aminer_author_profiles,
|
||||
"author_entries": author_entries,
|
||||
"source": _clean_text(raw.get("source") or "rec5"),
|
||||
"recommendation_reason": _clean_text(raw.get("recommendation_reason") or ""),
|
||||
}
|
||||
|
||||
|
||||
def call_rec5_api(
|
||||
params: dict[str, Any],
|
||||
*,
|
||||
token: str,
|
||||
url: str = DEFAULT_REC5_URL,
|
||||
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
|
||||
retry_attempts: int = DEFAULT_RETRY_ATTEMPTS,
|
||||
) -> dict[str, Any]:
|
||||
if not _clean_text(token):
|
||||
raise RuntimeError("missing_aminer_api_key")
|
||||
|
||||
body = json.dumps(params, ensure_ascii=False).encode("utf-8")
|
||||
ssl_context = ssl.create_default_context()
|
||||
opener = urllib.request.build_opener(
|
||||
urllib.request.ProxyHandler({}),
|
||||
urllib.request.HTTPSHandler(context=ssl_context),
|
||||
)
|
||||
|
||||
last_error: Exception | None = None
|
||||
for attempt in range(1, retry_attempts + 2):
|
||||
request = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
headers={
|
||||
"Content-Type": "application/json;charset=utf-8",
|
||||
"Authorization": token,
|
||||
"User-Agent": "aminer-rec/1.0",
|
||||
"X-Platform": "openclaw",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with opener.open(request, timeout=timeout_seconds) as response: # nosec B310
|
||||
payload = json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code in RETRYABLE_HTTP_CODES and attempt <= retry_attempts:
|
||||
last_error = exc
|
||||
time.sleep(0.5 * attempt)
|
||||
continue
|
||||
raise RuntimeError(f"rec5_api_http_{exc.code}") from exc
|
||||
except Exception as exc:
|
||||
if attempt <= retry_attempts:
|
||||
last_error = exc
|
||||
time.sleep(0.5 * attempt)
|
||||
continue
|
||||
raise RuntimeError(f"rec5_api_error:{exc.__class__.__name__}") from exc
|
||||
|
||||
if not payload.get("success"):
|
||||
msg = _clean_text(payload.get("msg") or str(payload.get("code") or "api_error"))
|
||||
raise RuntimeError(f"rec5_api_failed:{msg}")
|
||||
|
||||
data = payload.get("data")
|
||||
if isinstance(data, list) and data:
|
||||
papers = list(data[0].get("papers") or [])
|
||||
data_obj = data[0] if isinstance(data[0], dict) else {}
|
||||
elif isinstance(data, dict):
|
||||
papers = list(data.get("papers") or [])
|
||||
data_obj = data
|
||||
else:
|
||||
papers = []
|
||||
data_obj = {}
|
||||
analyzed_topics = list(data_obj.get("analyzed_topics") or []) if isinstance(data_obj, dict) else []
|
||||
return {
|
||||
"papers": [p for p in papers if isinstance(p, dict)],
|
||||
"analyzed_topics": [str(t).strip() for t in analyzed_topics if str(t).strip()],
|
||||
}
|
||||
|
||||
raise RuntimeError(f"rec5_api_unreachable:{_clean_text(str(last_error))}") from last_error
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
if __package__ in {None, ""}:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from scripts.common import write_json
|
||||
from scripts.constants import DEFAULT_TOP_K
|
||||
from scripts.rec5_api import (
|
||||
build_api_request,
|
||||
call_rec5_api,
|
||||
normalize_rec5_paper,
|
||||
resolve_rec5_url,
|
||||
resolve_token,
|
||||
)
|
||||
|
||||
|
||||
def _clean_text(value: Any) -> str:
|
||||
return " ".join(str(value or "").split()).strip()
|
||||
|
||||
|
||||
def _load_yaml(path: Path | None) -> dict[str, Any]:
|
||||
if path is None or not path.exists():
|
||||
return {}
|
||||
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
def _stage_error(stage: str, detail: Any) -> RuntimeError:
|
||||
compact = _clean_text(str(detail)) or "unknown_error"
|
||||
return RuntimeError(f"{stage}_failed:{compact}")
|
||||
|
||||
|
||||
def _format_papers_as_markdown(papers: list[dict[str, Any]], profile_topics: list[str]) -> str:
|
||||
"""Render recommended papers as Markdown for the host to display."""
|
||||
lines: list[str] = []
|
||||
topic_hint = " / ".join(profile_topics[:5]) if profile_topics else ""
|
||||
header = f"为你推荐 {len(papers)} 篇相关论文"
|
||||
if topic_hint:
|
||||
header += f"(研究方向:{topic_hint})"
|
||||
lines.append(header)
|
||||
|
||||
for idx, paper in enumerate(papers, start=1):
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
title = _clean_text(paper.get("title") or "")
|
||||
url = _clean_text(paper.get("aminer_paper_url") or paper.get("abs_url") or "")
|
||||
title_line = f"**{idx}. [{title}]({url})**" if url else f"**{idx}. {title}**"
|
||||
lines.append(title_line)
|
||||
|
||||
year = paper.get("year")
|
||||
keywords = paper.get("keywords") or []
|
||||
authors = paper.get("authors") or []
|
||||
summary = _clean_text(paper.get("summary") or "")
|
||||
|
||||
meta_parts: list[str] = []
|
||||
if year:
|
||||
meta_parts.append(f"年份:{year}")
|
||||
if keywords:
|
||||
meta_parts.append(f"关键词:{' / '.join(str(k) for k in keywords[:5])}")
|
||||
if meta_parts:
|
||||
lines.append(" | ".join(meta_parts))
|
||||
|
||||
if authors:
|
||||
author_str = "、".join(str(a) for a in authors[:6])
|
||||
if len(authors) > 6:
|
||||
author_str += " et al."
|
||||
lines.append(f"作者:{author_str}")
|
||||
|
||||
if summary:
|
||||
truncated = summary if len(summary) <= 300 else summary[:300].rstrip() + "…"
|
||||
lines.append("")
|
||||
lines.append(truncated)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _topics_from_paper_titles(paper_titles: list[str]) -> list[str]:
|
||||
"""Extract simple topic hints from paper titles when no explicit topics provided."""
|
||||
topics: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for title in paper_titles:
|
||||
cleaned = _clean_text(title)
|
||||
if not cleaned:
|
||||
continue
|
||||
candidate = cleaned.split(":")[0].strip() if ":" in cleaned else cleaned
|
||||
if candidate and candidate.casefold() not in seen and len(candidate) <= 80:
|
||||
seen.add(candidate.casefold())
|
||||
topics.append(candidate)
|
||||
return topics[:5]
|
||||
|
||||
|
||||
def run_pipeline(
|
||||
*,
|
||||
output_dir: Path,
|
||||
config: dict[str, Any],
|
||||
aminer_author_id: str,
|
||||
topics: list[str],
|
||||
scholar_name: str,
|
||||
scholar_org: str,
|
||||
paper_titles: list[str],
|
||||
papers_file: str,
|
||||
free_text: str,
|
||||
language_sort: str = "",
|
||||
size: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
token = resolve_token(config)
|
||||
if not token:
|
||||
raise _stage_error("auth", "AMINER_API_KEY is not set")
|
||||
|
||||
all_topics = list(topics)
|
||||
if paper_titles and not all_topics and not scholar_name and not aminer_author_id:
|
||||
all_topics = _topics_from_paper_titles(paper_titles)
|
||||
|
||||
search_config = config.get("search") if isinstance(config.get("search"), dict) else {}
|
||||
if size <= 0:
|
||||
size = max(1, min(int(search_config.get("top_k") or DEFAULT_TOP_K), 20))
|
||||
|
||||
# language_sort only when user explicitly passed language_sort: zh|en in the trigger (see SKILL).
|
||||
sort_for_api = _clean_text(language_sort) if _clean_text(language_sort) in {"zh", "en"} else ""
|
||||
|
||||
api_request = build_api_request(
|
||||
aminer_author_id=aminer_author_id,
|
||||
author_name=scholar_name,
|
||||
author_org=scholar_org,
|
||||
topics=all_topics,
|
||||
size=size,
|
||||
language_sort=sort_for_api,
|
||||
)
|
||||
|
||||
try:
|
||||
result = call_rec5_api(api_request, token=token, url=resolve_rec5_url(config))
|
||||
raw_papers = result["papers"]
|
||||
api_analyzed_topics = result.get("analyzed_topics") or []
|
||||
except Exception as exc:
|
||||
raise _stage_error("recall", exc) from exc
|
||||
|
||||
if not raw_papers:
|
||||
raise _stage_error("recall", "no_papers_returned")
|
||||
|
||||
papers = [normalize_rec5_paper(p) for p in raw_papers]
|
||||
papers = [p for p in papers if _clean_text(p.get("title"))]
|
||||
|
||||
profile_topics = api_analyzed_topics or all_topics or ([scholar_name] if scholar_name else [])
|
||||
summarized_payload = {
|
||||
"status": "success",
|
||||
"profile_topics": profile_topics,
|
||||
"profile_name": scholar_name or "",
|
||||
"profile_source": "scholar_path" if (aminer_author_id or scholar_name) else "topic_path",
|
||||
"papers": papers,
|
||||
}
|
||||
|
||||
mode = "scholar_path" if (aminer_author_id or scholar_name) else "topic_path"
|
||||
|
||||
write_json(
|
||||
output_dir / "request_context.json",
|
||||
{
|
||||
"aminer_author_id": aminer_author_id,
|
||||
"input_topics": topics,
|
||||
"topics": all_topics,
|
||||
"scholar_name": scholar_name,
|
||||
"scholar_org": scholar_org,
|
||||
"paper_titles": paper_titles,
|
||||
"papers_file": papers_file,
|
||||
"free_text": free_text,
|
||||
"language_sort": language_sort,
|
||||
"api_request": api_request,
|
||||
},
|
||||
)
|
||||
|
||||
summarized_path = output_dir / "papers_summarized.json"
|
||||
write_json(summarized_path, summarized_payload)
|
||||
markdown_text = _format_papers_as_markdown(papers, profile_topics)
|
||||
return {
|
||||
"status": "success",
|
||||
"summarized_path": str(summarized_path),
|
||||
"final_response": "TEXT",
|
||||
"reply_text": markdown_text,
|
||||
"mode": mode,
|
||||
"paper_count": len(papers),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run aminer-daily-paper pipeline via rec5 API.")
|
||||
parser.add_argument("--base-dir", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument("--config", type=Path, default=None)
|
||||
parser.add_argument("--output-dir", type=Path, default=Path(__file__).resolve().parents[1] / "outputs")
|
||||
parser.add_argument("--aminer-author-id", default="")
|
||||
parser.add_argument("--topics", nargs="*", default=[])
|
||||
parser.add_argument("--scholar-name", default="")
|
||||
parser.add_argument("--scholar-org", default="")
|
||||
parser.add_argument("--paper-title", action="append", dest="paper_titles", default=[])
|
||||
parser.add_argument("--papers-file", default="")
|
||||
parser.add_argument("--free-text", default="")
|
||||
parser.add_argument("--language-sort", default="")
|
||||
parser.add_argument("--size", type=int, default=0)
|
||||
args = parser.parse_args()
|
||||
|
||||
resolved_config = args.config.resolve() if args.config else None
|
||||
config = _load_yaml(resolved_config)
|
||||
|
||||
result = run_pipeline(
|
||||
output_dir=args.output_dir.resolve(),
|
||||
config=config,
|
||||
aminer_author_id=args.aminer_author_id,
|
||||
topics=list(args.topics or []),
|
||||
scholar_name=args.scholar_name,
|
||||
scholar_org=args.scholar_org,
|
||||
paper_titles=list(args.paper_titles or []),
|
||||
papers_file=args.papers_file,
|
||||
free_text=args.free_text,
|
||||
language_sort=args.language_sort,
|
||||
size=args.size,
|
||||
)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
outputs/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: aminer-deep-search
|
||||
version: 2.0.0
|
||||
author: AMiner
|
||||
contact: report@aminer.cn
|
||||
description: >
|
||||
Activate this skill when the user wants deep, multi-round academic paper collection for a survey or literature review.
|
||||
The host model (the model running this skill) drives the loop itself: it expands queries, judges relevance, snowballs backward references, and decides when to stop.
|
||||
The bundled scripts are pure tool commands that call documented AMiner Open Platform endpoints and print JSON tool results only — no extra LLM configuration is needed.
|
||||
Use this skill for broad topic exploration, survey bibliography construction, and collecting hundreds of candidate papers with AMiner IDs and titles.
|
||||
Not intended for single-paper lookup or lightweight recommendations; use aminer-free-academic or aminer-daily-paper for those.
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"requires": {
|
||||
"bins": ["python3"],
|
||||
"env": ["AMINER_API_KEY"]
|
||||
},
|
||||
"primaryEnv": "AMINER_API_KEY"
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
# AMiner Deep Search
|
||||
|
||||
Host-model-driven survey paper collection. You (the model reading this) are the controller: run the tool scripts, read their JSON output, judge relevance yourself, and iterate until the collection target is met.
|
||||
|
||||
## Scope
|
||||
|
||||
- Use for: survey bibliography collection (hundreds of papers), keyword expansion, backward-citation snowballing.
|
||||
- Do not use for: single-paper lookup or Q&A (route to `aminer-free-academic`), personalized recommendations (route to `aminer-daily-paper`).
|
||||
|
||||
## Pre-flight
|
||||
|
||||
1. Check the key without printing it:
|
||||
|
||||
```bash
|
||||
[ -z "${AMINER_API_KEY:-}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"
|
||||
```
|
||||
|
||||
If missing, stop and ask the user to set `AMINER_API_KEY` (console: https://open.aminer.cn/open/board?tab=control). Never print the key.
|
||||
|
||||
2. Confirm the `topic` and the `target-size` (default 400). If your round plan is estimated to cost ¥5 or more, tell the user the estimate and get confirmation before starting.
|
||||
|
||||
## Tools
|
||||
|
||||
Both scripts live in `scripts/` under this skill directory. They print exactly one JSON document to stdout (the tool result); diagnostics and a `[cost]` line go to stderr. They never score relevance — that is your job.
|
||||
|
||||
### `scripts/aminer_api.py` — AMiner API calls
|
||||
|
||||
| Subcommand | Endpoint | Price |
|
||||
|---|---|---|
|
||||
| `search --query Q [--size 20] [--year YYYY] [--order n_citation\|year] [--max-pages 3]` | GET `/api/paper/search/pro` + free `paper/info` enrichment | ¥0.01/page |
|
||||
| `qa-search [--query "natural language question"] [--topic-high '[["termA","termB"],["termC"]]'] [--size 20] [--year-from Y] [--year-to Y] [--citation-sort]` | POST `/api/paper/qa/search` (always `use_topic=true`; the backend ignores `query` when `use_topic=false`) + free enrichment | ¥0.05/call |
|
||||
| `info --ids id1 id2 ...` | POST `/api/paper/info` (batched ≤100 ids) | Free |
|
||||
| `references --ids id1 id2 ... [--per-seed 20]` | GET `/api/paper/relation` per seed + free enrichment | ¥0.10/seed |
|
||||
|
||||
Output shape: `search`/`qa-search`/`info` print `[{id, title, year?, venue?, abstract_slice?}]`; `references` additionally includes `source_paper_ids` (which seeds cited the paper). Seeds themselves are excluded from `references` output.
|
||||
|
||||
### `scripts/paper_set.py` — cross-round state file (no network)
|
||||
|
||||
State file defaults to `outputs/paper_set.json` relative to the working directory.
|
||||
|
||||
```bash
|
||||
# Merge kept results (pipe the filtered JSON array in), dedupe by id
|
||||
python3 scripts/aminer_api.py search --query "..." | python3 scripts/paper_set.py add
|
||||
# → {"added": N, "duplicates": M, "total": T}
|
||||
|
||||
python3 scripts/paper_set.py stats # totals, expanded_seeds, by_year
|
||||
python3 scripts/paper_set.py mark-expanded --ids id1 id2 # record snowballed seeds
|
||||
python3 scripts/paper_set.py export -o outputs/final_papers.json
|
||||
```
|
||||
|
||||
`add` also accepts `--ids id1 id2 ...` for bare IDs. Items carrying `source_paper_ids` (from `references`) automatically mark those seeds as expanded.
|
||||
|
||||
If you want to filter before adding, read the search output first, then pipe only the kept items:
|
||||
|
||||
```bash
|
||||
printf '%s' '[{"id":"...","title":"..."}]' | python3 scripts/paper_set.py add
|
||||
```
|
||||
|
||||
## Round Protocol (core)
|
||||
|
||||
### Round 0 — plan
|
||||
|
||||
- Derive 4–8 seed queries from the topic: synonyms, subfields, method names, datasets/benchmarks, common English abbreviations.
|
||||
- Estimate rounds and cost (searches ≈ ¥0.01–0.05 each, references ≈ ¥0.10/seed). If the estimate is ≥¥5, confirm with the user first.
|
||||
|
||||
### Each round (default budget: 12 rounds), five fixed steps
|
||||
|
||||
1. **Search**: run 1–4 `search` / `qa-search` calls from the pending query queue. Prefer `search` (cheaper); use `qa-search` when the query is a natural-language question.
|
||||
2. **Filter & add**: read the stdout results, judge relevance to the topic yourself, and pipe only the kept items into `paper_set.py add`. Never add papers you consider off-topic.
|
||||
3. **Check**: run `stats` to see the total and this round's increment.
|
||||
4. **Snowball**: from this round's relevant additions pick ≤5 strong seeds (highly relevant, ranked high under `--order n_citation`, not in `expanded_seeds`) and run `references --ids ...`. Filter the output for relevance, then add it. Run `mark-expanded` for seeds that yielded nothing addable.
|
||||
5. **Decide**: choose the next move —
|
||||
- a search returned <5 results or poor quality → replace it with a reformulated query (max 2 variants per direction, then switch to snowballing);
|
||||
- references are yielding many relevant papers → keep snowballing from fresh seeds;
|
||||
- reached `target-size`, or results are exhausted, or 2 consecutive rounds added <5 papers → terminate.
|
||||
|
||||
### Wrap-up
|
||||
|
||||
Run `export`, then report: final paper count, total cost (sum the `[cost]` stderr lines), and the output path.
|
||||
|
||||
## Rules
|
||||
|
||||
1. Never fabricate paper IDs or titles; only cite data actually returned by the tools.
|
||||
2. Free first: metadata always comes from the free `paper/info` (the scripts already do this); never call the paid `paper/detail` for bulk metadata.
|
||||
3. Keep the raw tool output out of your final answer; report counts and the exported file path instead.
|
||||
4. Never print or log `AMINER_API_KEY`.
|
||||
5. If AMiner returns fewer papers than the target, report the real count instead of inventing papers.
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: aminer-deep-search
|
||||
version: 2.0.0
|
||||
author: AMiner
|
||||
contact: report@aminer.cn
|
||||
description: >
|
||||
当用户需要为综述或文献回顾做深度、多轮的学术论文收集时激活本 skill。
|
||||
由宿主模型(正在运行本 skill 的模型)亲自驱动循环:扩展查询、判断相关性、做引用滚雪球、决定何时终止。
|
||||
附带脚本只是纯工具命令,调用 AMiner 开放平台的正规接口并只输出 JSON tool-result,无需配置任何额外 LLM。
|
||||
适用于大范围主题探索、综述参考文献构建、收集数百篇带 AMiner ID 与标题的候选论文。
|
||||
不适用于单篇论文查询或轻量推荐,请分别使用 aminer-free-academic 或 aminer-daily-paper。
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"requires": {
|
||||
"bins": ["python3"],
|
||||
"env": ["AMINER_API_KEY"]
|
||||
},
|
||||
"primaryEnv": "AMINER_API_KEY"
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
# AMiner 深度收集
|
||||
|
||||
宿主模型直驱的综述文献收集。你(正在读这份文档的模型)就是控制器:运行工具脚本、阅读 JSON 输出、自己判断相关性、迭代直到达成收集目标。
|
||||
|
||||
## 适用边界
|
||||
|
||||
- 适用:综述级文献收集(数百篇)、关键词扩展、后向引用滚雪球。
|
||||
- 不适用:单篇查询或问答(走 `aminer-free-academic`)、个性化推荐(走 `aminer-daily-paper`)。
|
||||
|
||||
## Pre-flight
|
||||
|
||||
1. 检查 key(不打印值):
|
||||
|
||||
```bash
|
||||
[ -z "${AMINER_API_KEY:-}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"
|
||||
```
|
||||
|
||||
缺失时停止并让用户设置 `AMINER_API_KEY`(控制台:https://open.aminer.cn/open/board?tab=control)。绝不打印 key。
|
||||
|
||||
2. 确认 `topic` 和 `target-size`(默认 400)。若轮次计划预估费用 ≥¥5,先告知用户并获得确认再开始。
|
||||
|
||||
## 工具
|
||||
|
||||
两个脚本都在本 skill 目录的 `scripts/` 下。stdout 只输出一个 JSON 文档(即 tool-result);诊断信息与 `[cost]` 费用行走 stderr。脚本不做任何相关性打分——那是你的工作。
|
||||
|
||||
### `scripts/aminer_api.py` — AMiner 接口调用
|
||||
|
||||
| 子命令 | 端点 | 价格 |
|
||||
|---|---|---|
|
||||
| `search --query Q [--size 20] [--year YYYY] [--order n_citation\|year] [--max-pages 3]` | GET `/api/paper/search/pro` + 免费 `paper/info` 补全 | ¥0.01/页 |
|
||||
| `qa-search [--query "自然语言问题"] [--topic-high '[["词A","词B"],["词C"]]'] [--size 20] [--year-from Y] [--year-to Y] [--citation-sort]` | POST `/api/paper/qa/search`(固定 `use_topic=true`:`use_topic=false` 时后端忽略 `query`)+ 免费补全 | ¥0.05/次 |
|
||||
| `info --ids id1 id2 ...` | POST `/api/paper/info`(≤100 个 id 分批) | 免费 |
|
||||
| `references --ids id1 id2 ... [--per-seed 20]` | 每个 seed 调 GET `/api/paper/relation` + 免费补全 | ¥0.10/篇 seed |
|
||||
|
||||
输出形状:`search`/`qa-search`/`info` 输出 `[{id, title, year?, venue?, abstract_slice?}]`;`references` 额外带 `source_paper_ids`(哪些 seed 引用了该论文),且结果中排除 seed 本身。
|
||||
|
||||
### `scripts/paper_set.py` — 跨轮状态文件(无网络)
|
||||
|
||||
状态文件默认是工作目录下的 `outputs/paper_set.json`。
|
||||
|
||||
```bash
|
||||
# 把保留的结果管道进去(可直接接在搜索后面),按 id 去重
|
||||
python3 scripts/aminer_api.py search --query "..." | python3 scripts/paper_set.py add
|
||||
# → {"added": N, "duplicates": M, "total": T}
|
||||
|
||||
python3 scripts/paper_set.py stats # 总量、expanded_seeds、按年分布
|
||||
python3 scripts/paper_set.py mark-expanded --ids id1 id2 # 记录已滚雪球的 seed
|
||||
python3 scripts/paper_set.py export -o outputs/final_papers.json
|
||||
```
|
||||
|
||||
`add` 也接受 `--ids id1 id2 ...` 直接加裸 id。带 `source_paper_ids` 的条目(来自 `references`)会自动把对应 seed 记为已扩展。
|
||||
|
||||
如需先筛选再入库,先阅读搜索输出,再只把保留的条目管道进去:
|
||||
|
||||
```bash
|
||||
printf '%s' '[{"id":"...","title":"..."}]' | python3 scripts/paper_set.py add
|
||||
```
|
||||
|
||||
## 每轮协议(核心)
|
||||
|
||||
### 第 0 轮 — 规划
|
||||
|
||||
- 从 topic 派生 4–8 个种子查询:同义词、子领域、方法名、数据集/基准、常用英文缩写。
|
||||
- 预估轮数与费用(搜索约 ¥0.01–0.05/次,references 约 ¥0.10/seed)。预估 ≥¥5 时先向用户确认。
|
||||
|
||||
### 每一轮(默认最多 12 轮),固定五步
|
||||
|
||||
1. **搜索**:对待查队列执行 1–4 个 `search` / `qa-search`。优先用 `search`(更便宜);查询是自然语言问题时用 `qa-search`。
|
||||
2. **筛选入库**:阅读 stdout 结果,自己判断与主题的相关性,只把保留的条目管道进 `paper_set.py add`。绝不入库你认为跑题的论文。
|
||||
3. **查看进度**:跑 `stats` 查看总量与本轮增量。
|
||||
4. **滚雪球**:从本轮新增的相关论文里挑 ≤5 个强种子(高相关、`--order n_citation` 排前、不在 `expanded_seeds` 中),跑 `references --ids ...`。对输出再做相关性筛选后入库。没有可入库产出的 seed 用 `mark-expanded` 记录。
|
||||
5. **决策下一轮**:
|
||||
- 某个搜索结果 <5 条或质量差 → 换一个改写后的查询(每个方向最多试 2 个变体,之后转滚雪球);
|
||||
- references 持续产出大量相关论文 → 继续从新 seed 滚雪球;
|
||||
- 达到 `target-size`、结果枯竭、或连续 2 轮新增 <5 篇 → 终止。
|
||||
|
||||
### 收尾
|
||||
|
||||
跑 `export`,然后报告:最终篇数、总费用(累加 stderr 的 `[cost]` 行)、输出文件路径。
|
||||
|
||||
## 规则
|
||||
|
||||
1. 绝不编造论文 ID 或标题;只引用工具实际返回的数据。
|
||||
2. 免费优先:元数据一律来自免费的 `paper/info`(脚本已内置);绝不为批量元数据调付费的 `paper/detail`。
|
||||
3. 不要把原始工具输出塞进最终回答;只报告数量与导出文件路径。
|
||||
4. 绝不打印或记录 `AMINER_API_KEY`。
|
||||
5. 如果 AMiner 返回的论文少于目标,如实报告实际数量,不要编造。
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
description: AMiner deep multi-round paper collection for survey references
|
||||
argument-hint: "[research topic | topic: ... target-size: 400 max-rounds: 12]"
|
||||
allowed-tools: Read, Bash, Glob, Grep
|
||||
---
|
||||
|
||||
# /aminer-deep-search - AMiner Deep Search
|
||||
|
||||
User invoked the AMiner deep paper collection skill with the following arguments:
|
||||
|
||||
```text
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
## Your task
|
||||
|
||||
Follow `${CLAUDE_PLUGIN_ROOT}/SKILL.md`. You are the controller: run the tool scripts, read their JSON output, judge relevance yourself, and iterate. There is no external LLM mode and nothing to configure beyond `AMINER_API_KEY`.
|
||||
|
||||
Use this command only for deep survey-style paper collection, not for simple paper lookup or lightweight recommendations.
|
||||
|
||||
### 1. Parse `$ARGUMENTS`
|
||||
|
||||
- `topic`: required research topic. Preserve the user's wording. If absent or too vague, ask for a concrete topic.
|
||||
- `target-size`: optional final paper target, default 400.
|
||||
- `max-rounds`: optional round budget, default 12.
|
||||
|
||||
### 2. Pre-flight
|
||||
|
||||
```bash
|
||||
[ -z "${AMINER_API_KEY:-}" ] && echo "AMINER_API_KEY missing" || echo "AMINER_API_KEY exists"
|
||||
```
|
||||
|
||||
If missing, stop and tell the user to set `AMINER_API_KEY`. Never print the key. The scripts are pure stdlib — no dependency installation is needed.
|
||||
|
||||
### 3. Run the round protocol
|
||||
|
||||
Execute the Round Protocol from `${CLAUDE_PLUGIN_ROOT}/SKILL.md`:
|
||||
|
||||
- Round 0: derive 4–8 seed queries, estimate cost; confirm with the user if the estimate is ≥¥5.
|
||||
- Each round (max `max-rounds`): search via
|
||||
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/aminer_api.py" search --query "..." --order n_citation`,
|
||||
filter results for relevance yourself, add the kept items via
|
||||
`python3 "${CLAUDE_PLUGIN_ROOT}/scripts/paper_set.py" add`,
|
||||
snowball with `references --ids ...` on ≤5 strong unexpanded seeds, and check `stats`.
|
||||
- Stop when `target-size` is reached, results are exhausted, or 2 consecutive rounds add <5 papers.
|
||||
|
||||
Keep the state file and exports under the current working directory (`outputs/`).
|
||||
|
||||
### 4. Present the result
|
||||
|
||||
Run `export` and report the final paper count, the total cost (sum of `[cost]` stderr lines), and the output path. If the run fails due to missing configuration or API errors, show the actionable error without exposing secrets. Never fabricate papers.
|
||||
+410
@@ -0,0 +1,410 @@
|
||||
#!/usr/bin/env python3
|
||||
"""AMiner Open Platform tool CLI for aminer-deep-search.
|
||||
|
||||
Pure-stdlib tool commands driven by the host model. Every subcommand prints a
|
||||
single JSON document to stdout (the tool result); diagnostics and the cost
|
||||
summary go to stderr. No relevance scoring or loop control happens here — the
|
||||
host model decides what to search, what to keep, and when to stop.
|
||||
|
||||
Endpoints (base: https://publicapi.chatglm.cn/chatglm_public/skill/aminer):
|
||||
GET /api/paper/search/pro keyword search, ¥0.01/page (subcommand: search)
|
||||
POST /api/paper/qa/search natural-language search, ¥0.05/call (qa-search)
|
||||
POST /api/paper/info batch metadata, free, <=100 ids (info, and enrichment)
|
||||
GET /api/paper/relation backward references, ¥0.10/seed (references)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
BASE_URL = "https://publicapi.chatglm.cn/chatglm_public/skill/aminer"
|
||||
CONSOLE_URL = "https://open.aminer.cn/open/board?tab=control"
|
||||
TIMEOUT_SECONDS = 30.0
|
||||
MAX_RETRIES = 3
|
||||
RETRYABLE_STATUS = frozenset({408, 429, 500, 502, 503, 504})
|
||||
PAPER_INFO_BATCH_LIMIT = 100
|
||||
SEARCH_PRO_PAGE_SIZE = 20
|
||||
ABSTRACT_SLICE_LIMIT = 300
|
||||
|
||||
PRICE_CNY = {
|
||||
"search_pro": 0.01,
|
||||
"qa_search": 0.05,
|
||||
"paper_info": 0.0,
|
||||
"relation": 0.10,
|
||||
}
|
||||
|
||||
_call_counts: dict[str, int] = {}
|
||||
|
||||
|
||||
class ToolError(Exception):
|
||||
"""Fatal error carrying a structured JSON payload for stdout."""
|
||||
|
||||
def __init__(self, payload: dict[str, Any], exit_code: int = 1) -> None:
|
||||
super().__init__(payload.get("message", "tool error"))
|
||||
self.payload = payload
|
||||
self.exit_code = exit_code
|
||||
|
||||
|
||||
def _get_token() -> str:
|
||||
token = (os.getenv("AMINER_API_KEY") or "").strip()
|
||||
if not token:
|
||||
raise ToolError(
|
||||
{
|
||||
"ok": False,
|
||||
"error": "missing_aminer_api_key",
|
||||
"message": "Set the AMINER_API_KEY environment variable before calling AMiner APIs.",
|
||||
"console": CONSOLE_URL,
|
||||
},
|
||||
exit_code=2,
|
||||
)
|
||||
return token
|
||||
|
||||
|
||||
def _redact(value: Any, token: str) -> Any:
|
||||
if isinstance(value, str):
|
||||
return value.replace(token, "[REDACTED]") if token else value
|
||||
if isinstance(value, list):
|
||||
return [_redact(item, token) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: _redact(item, token) for key, item in value.items()}
|
||||
return value
|
||||
|
||||
|
||||
def _request(api_name: str, method: str, path: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
body: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""Call one endpoint with retries; return the parsed JSON envelope."""
|
||||
token = _get_token()
|
||||
url = BASE_URL + path
|
||||
if method == "GET" and params:
|
||||
query = urllib.parse.urlencode(
|
||||
{k: v for k, v in params.items() if v is not None}
|
||||
)
|
||||
url = f"{url}?{query}"
|
||||
data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
|
||||
headers = {"Authorization": token, "X-Platform": "openclaw"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json;charset=utf-8"
|
||||
|
||||
_call_counts[api_name] = _call_counts.get(api_name, 0) + 1
|
||||
|
||||
last_error: dict[str, Any] | None = None
|
||||
for attempt in range(1, MAX_RETRIES + 1):
|
||||
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) as response:
|
||||
raw = response.read().decode("utf-8", errors="replace")
|
||||
envelope = json.loads(raw)
|
||||
if not isinstance(envelope, dict):
|
||||
return {"data": envelope}
|
||||
# AMiner can wrap errors in an HTTP-200 response, e.g. code 403
|
||||
# for insufficient balance/permission. Surface those instead of
|
||||
# letting them look like an empty result set.
|
||||
code = envelope.get("code")
|
||||
if isinstance(code, int) and code not in (0, 200):
|
||||
raise ToolError({
|
||||
"ok": False,
|
||||
"error": "api_error",
|
||||
"api": api_name,
|
||||
"code": code,
|
||||
"message": _redact(str(envelope.get("msg") or ""), token)[:300],
|
||||
})
|
||||
return envelope
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = _redact(exc.read().decode("utf-8", errors="replace")[:500], token)
|
||||
last_error = {
|
||||
"ok": False,
|
||||
"error": "http_error",
|
||||
"api": api_name,
|
||||
"status": exc.code,
|
||||
"detail": detail,
|
||||
}
|
||||
if exc.code not in RETRYABLE_STATUS:
|
||||
break
|
||||
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
||||
last_error = {
|
||||
"ok": False,
|
||||
"error": "network_error",
|
||||
"api": api_name,
|
||||
"detail": _redact(str(getattr(exc, "reason", exc)), token)[:500],
|
||||
}
|
||||
except json.JSONDecodeError as exc:
|
||||
last_error = {
|
||||
"ok": False,
|
||||
"error": "invalid_response",
|
||||
"api": api_name,
|
||||
"detail": str(exc)[:200],
|
||||
}
|
||||
break
|
||||
if attempt < MAX_RETRIES:
|
||||
backoff = (2 ** (attempt - 1)) + random.uniform(0, 0.3)
|
||||
print(f"[retry] {api_name} attempt={attempt}/{MAX_RETRIES} wait={backoff:.1f}s",
|
||||
file=sys.stderr)
|
||||
time.sleep(backoff)
|
||||
|
||||
raise ToolError(last_error or {"ok": False, "error": "request_failed", "api": api_name})
|
||||
|
||||
|
||||
def _envelope_items(envelope: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
data = envelope.get("data")
|
||||
if isinstance(data, dict):
|
||||
data = data.get("data") or data.get("items") or []
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
return [item for item in data if isinstance(item, dict)]
|
||||
|
||||
|
||||
def _dedupe(items: list[str]) -> list[str]:
|
||||
seen: set[str] = set()
|
||||
result: list[str] = []
|
||||
for item in items:
|
||||
cleaned = str(item or "").strip()
|
||||
if cleaned and cleaned not in seen:
|
||||
seen.add(cleaned)
|
||||
result.append(cleaned)
|
||||
return result
|
||||
|
||||
|
||||
def _paper_id(item: dict[str, Any]) -> str:
|
||||
return str(item.get("id") or item.get("_id") or "").strip()
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# paper/info enrichment (free)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _fetch_paper_info(ids: list[str]) -> dict[str, dict[str, Any]]:
|
||||
"""Batch-fetch lightweight cards; returns a map of id -> raw info entry."""
|
||||
info_by_id: dict[str, dict[str, Any]] = {}
|
||||
unique_ids = _dedupe(ids)
|
||||
for start in range(0, len(unique_ids), PAPER_INFO_BATCH_LIMIT):
|
||||
chunk = unique_ids[start:start + PAPER_INFO_BATCH_LIMIT]
|
||||
envelope = _request("paper_info", "POST", "/api/paper/info", body={"ids": chunk})
|
||||
for entry in _envelope_items(envelope):
|
||||
entry_id = _paper_id(entry)
|
||||
if entry_id:
|
||||
info_by_id[entry_id] = entry
|
||||
return info_by_id
|
||||
|
||||
|
||||
def _venue_text(entry: dict[str, Any]) -> str:
|
||||
venue = entry.get("venue")
|
||||
if isinstance(venue, dict):
|
||||
return str(venue.get("raw") or venue.get("name") or "")
|
||||
return str(venue or entry.get("raw") or entry.get("venue_name") or "")
|
||||
|
||||
|
||||
def _compact_paper(base: dict[str, Any], info: dict[str, Any] | None) -> dict[str, Any]:
|
||||
merged: dict[str, Any] = dict(info or {})
|
||||
item: dict[str, Any] = {
|
||||
"id": _paper_id(base) or _paper_id(merged),
|
||||
"title": str(base.get("title") or merged.get("title")
|
||||
or base.get("title_zh") or merged.get("title_zh") or "").strip(),
|
||||
}
|
||||
year = merged.get("year") or base.get("year")
|
||||
if year:
|
||||
item["year"] = year
|
||||
venue = _venue_text(merged) or _venue_text(base)
|
||||
if venue:
|
||||
item["venue"] = venue
|
||||
abstract = str(merged.get("abstract_slice") or merged.get("abstract") or "").strip()
|
||||
if abstract:
|
||||
item["abstract_slice"] = abstract[:ABSTRACT_SLICE_LIMIT]
|
||||
return item
|
||||
|
||||
|
||||
def _enrich(base_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Attach year/venue/abstract_slice from the free paper/info endpoint."""
|
||||
ids = [_paper_id(item) for item in base_items if _paper_id(item)]
|
||||
info_by_id = _fetch_paper_info(ids) if ids else {}
|
||||
papers: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for item in base_items:
|
||||
paper_id = _paper_id(item)
|
||||
if not paper_id or paper_id in seen:
|
||||
continue
|
||||
seen.add(paper_id)
|
||||
compact = _compact_paper(item, info_by_id.get(paper_id))
|
||||
if compact["id"] and compact["title"]:
|
||||
papers.append(compact)
|
||||
return papers
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Subcommands
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def cmd_search(args: argparse.Namespace) -> list[dict[str, Any]]:
|
||||
target = max(1, args.size)
|
||||
max_year = args.year
|
||||
collected: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for page in range(max(1, args.max_pages)):
|
||||
params: dict[str, Any] = {
|
||||
"keyword": args.query,
|
||||
"page": page,
|
||||
"size": SEARCH_PRO_PAGE_SIZE,
|
||||
}
|
||||
if args.order:
|
||||
params["order"] = args.order
|
||||
envelope = _request("search_pro", "GET", "/api/paper/search/pro", params=params)
|
||||
page_items = _envelope_items(envelope)
|
||||
if not page_items:
|
||||
break
|
||||
enriched = _enrich(page_items)
|
||||
for paper in enriched:
|
||||
if paper["id"] in seen:
|
||||
continue
|
||||
if max_year and paper.get("year") and int(paper["year"]) > max_year:
|
||||
continue
|
||||
seen.add(paper["id"])
|
||||
collected.append(paper)
|
||||
if len(collected) >= target or len(page_items) < SEARCH_PRO_PAGE_SIZE:
|
||||
break
|
||||
return collected[:target]
|
||||
|
||||
|
||||
def cmd_qa_search(args: argparse.Namespace) -> list[dict[str, Any]]:
|
||||
# The backend only uses the `query` keyword-extraction result when
|
||||
# use_topic=true; with use_topic=false it reads `title` only, so a
|
||||
# query-only request ends up with empty terms and returns envelope 403.
|
||||
# Therefore always send use_topic=true.
|
||||
body: dict[str, Any] = {
|
||||
"use_topic": True,
|
||||
"size": max(1, args.size),
|
||||
}
|
||||
if args.query:
|
||||
body["query"] = args.query
|
||||
if args.topic_high:
|
||||
body["topic_high"] = args.topic_high
|
||||
if not args.query and not args.topic_high:
|
||||
raise ToolError({
|
||||
"ok": False,
|
||||
"error": "invalid_request",
|
||||
"message": "qa-search requires --query and/or --topic-high",
|
||||
}, exit_code=2)
|
||||
if args.year_from or args.year_to:
|
||||
year_from = args.year_from or 1900
|
||||
year_to = args.year_to or time.gmtime().tm_year
|
||||
body["year"] = list(range(year_from, year_to + 1))
|
||||
if args.citation_sort:
|
||||
body["force_citation_sort"] = True
|
||||
envelope = _request("qa_search", "POST", "/api/paper/qa/search", body=body)
|
||||
return _enrich(_envelope_items(envelope))
|
||||
|
||||
|
||||
def cmd_info(args: argparse.Namespace) -> list[dict[str, Any]]:
|
||||
info_by_id = _fetch_paper_info(args.ids)
|
||||
return [_compact_paper(entry, None) for entry in info_by_id.values()]
|
||||
|
||||
|
||||
def cmd_references(args: argparse.Namespace) -> list[dict[str, Any]]:
|
||||
seed_ids = _dedupe(args.ids)
|
||||
per_seed = max(1, args.per_seed)
|
||||
sources_by_id: dict[str, list[str]] = {}
|
||||
ordered_ids: list[str] = []
|
||||
seed_set = set(seed_ids)
|
||||
for seed_id in seed_ids:
|
||||
envelope = _request("relation", "GET", "/api/paper/relation",
|
||||
params={"id": seed_id})
|
||||
cited_ids: list[str] = []
|
||||
for item in _envelope_items(envelope):
|
||||
cited = item.get("cited") or []
|
||||
if not isinstance(cited, list):
|
||||
continue
|
||||
for cited_item in cited:
|
||||
cited_id = _paper_id(cited_item) if isinstance(cited_item, dict) else str(cited_item or "").strip()
|
||||
if cited_id:
|
||||
cited_ids.append(cited_id)
|
||||
for cited_id in _dedupe(cited_ids)[:per_seed]:
|
||||
if cited_id in seed_set:
|
||||
continue
|
||||
if cited_id not in sources_by_id:
|
||||
sources_by_id[cited_id] = []
|
||||
ordered_ids.append(cited_id)
|
||||
sources_by_id[cited_id].append(seed_id)
|
||||
|
||||
papers = _enrich([{"id": paper_id} for paper_id in ordered_ids])
|
||||
for paper in papers:
|
||||
paper["source_paper_ids"] = sources_by_id.get(paper["id"], [])
|
||||
return papers
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# CLI
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="AMiner Open Platform tool commands (stdout: JSON only; diagnostics: stderr)."
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_search = sub.add_parser("search", help="Keyword search via paper/search/pro (¥0.01/page).")
|
||||
p_search.add_argument("--query", required=True)
|
||||
p_search.add_argument("--size", type=int, default=20, help="Papers to return (default 20).")
|
||||
p_search.add_argument("--year", type=int, default=None,
|
||||
help="Inclusive publication end year (client-side filter).")
|
||||
p_search.add_argument("--order", choices=["n_citation", "year"], default=None)
|
||||
p_search.add_argument("--max-pages", type=int, default=3,
|
||||
help="Max result pages to fetch (default 3).")
|
||||
p_search.set_defaults(func=cmd_search)
|
||||
|
||||
p_qa = sub.add_parser("qa-search", help="Natural-language search via paper/qa/search (¥0.05/call).")
|
||||
p_qa.add_argument("--query", default=None, help="Natural-language question.")
|
||||
p_qa.add_argument("--topic-high", default=None,
|
||||
help='Structured keywords, JSON string like [["termA","termB"],["termC"]] '
|
||||
"(outer AND, inner OR).")
|
||||
p_qa.add_argument("--size", type=int, default=20)
|
||||
p_qa.add_argument("--year-from", type=int, default=None)
|
||||
p_qa.add_argument("--year-to", type=int, default=None)
|
||||
p_qa.add_argument("--citation-sort", action="store_true",
|
||||
help="Sort entirely by citation count.")
|
||||
p_qa.set_defaults(func=cmd_qa_search)
|
||||
|
||||
p_info = sub.add_parser("info", help="Batch metadata via paper/info (free, <=100 ids/batch).")
|
||||
p_info.add_argument("--ids", nargs="+", required=True)
|
||||
p_info.set_defaults(func=cmd_info)
|
||||
|
||||
p_refs = sub.add_parser("references",
|
||||
help="Backward-reference expansion via paper/relation (¥0.10/seed).")
|
||||
p_refs.add_argument("--ids", nargs="+", required=True, help="Seed AMiner paper IDs.")
|
||||
p_refs.add_argument("--per-seed", type=int, default=20,
|
||||
help="Max references kept per seed (default 20).")
|
||||
p_refs.set_defaults(func=cmd_references)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _print_cost_summary() -> None:
|
||||
if not _call_counts:
|
||||
return
|
||||
total = sum(PRICE_CNY.get(name, 0.0) * count for name, count in _call_counts.items())
|
||||
parts = " + ".join(f"{name} x{count}" for name, count in sorted(_call_counts.items()))
|
||||
print(f"[cost] {parts} = ¥{total:.2f}", file=sys.stderr)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
result = args.func(args)
|
||||
except ToolError as exc:
|
||||
print(json.dumps(exc.payload, ensure_ascii=False), file=sys.stdout)
|
||||
_print_cost_summary()
|
||||
return exc.exit_code
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
_print_cost_summary()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Cross-round paper-set state file for aminer-deep-search (no network access).
|
||||
|
||||
Hundreds of collected papers do not fit in the host model's context, so this
|
||||
CLI keeps them in a JSON state file and deduplicates by AMiner paper ID.
|
||||
|
||||
Subcommands (stdout is always a single JSON document):
|
||||
add merge papers from stdin (JSON array) or --ids into the state file
|
||||
stats totals, expanded seeds, and year distribution
|
||||
export write the final [{"id","title"}] list and report path + count
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_STATE_FILE = "outputs/paper_set.json"
|
||||
|
||||
|
||||
def _load_state(path: Path) -> dict[str, Any]:
|
||||
if not path.exists():
|
||||
return {"papers": {}, "expanded_seeds": []}
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
state = json.load(file)
|
||||
state.setdefault("papers", {})
|
||||
state.setdefault("expanded_seeds", [])
|
||||
return state
|
||||
|
||||
|
||||
def _save_state(path: Path, state: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as file:
|
||||
json.dump(state, file, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def _normalize_items(raw_items: Any) -> list[dict[str, Any]]:
|
||||
if not isinstance(raw_items, list):
|
||||
raise ValueError("input must be a JSON array of papers")
|
||||
items: list[dict[str, Any]] = []
|
||||
for raw in raw_items:
|
||||
if isinstance(raw, str):
|
||||
items.append({"id": raw.strip()})
|
||||
elif isinstance(raw, dict):
|
||||
items.append(raw)
|
||||
return items
|
||||
|
||||
|
||||
def cmd_add(args: argparse.Namespace) -> dict[str, Any]:
|
||||
if args.ids:
|
||||
items = [{"id": paper_id} for paper_id in args.ids]
|
||||
else:
|
||||
stdin_text = sys.stdin.read().strip()
|
||||
if not stdin_text:
|
||||
raise ValueError("no input: pipe a JSON array to stdin or pass --ids")
|
||||
items = _normalize_items(json.loads(stdin_text))
|
||||
|
||||
state_path = Path(args.file)
|
||||
state = _load_state(state_path)
|
||||
papers: dict[str, dict[str, Any]] = state["papers"]
|
||||
expanded: set[str] = set(state["expanded_seeds"])
|
||||
|
||||
added = 0
|
||||
duplicates = 0
|
||||
for item in items:
|
||||
paper_id = str(item.get("id") or item.get("_id") or "").strip()
|
||||
if not paper_id:
|
||||
continue
|
||||
# source_paper_ids on an item means those seeds have been expanded
|
||||
for seed_id in item.get("source_paper_ids") or []:
|
||||
expanded.add(str(seed_id))
|
||||
if paper_id in papers:
|
||||
duplicates += 1
|
||||
existing = papers[paper_id]
|
||||
for key, value in item.items():
|
||||
if key != "source_paper_ids" and value not in (None, "", []):
|
||||
existing.setdefault(key, value)
|
||||
else:
|
||||
added += 1
|
||||
papers[paper_id] = {
|
||||
key: value for key, value in item.items()
|
||||
if key != "source_paper_ids" and value not in (None, "", [])
|
||||
}
|
||||
|
||||
state["expanded_seeds"] = sorted(expanded)
|
||||
_save_state(state_path, state)
|
||||
return {"added": added, "duplicates": duplicates, "total": len(papers)}
|
||||
|
||||
|
||||
def cmd_mark_expanded(args: argparse.Namespace) -> dict[str, Any]:
|
||||
state_path = Path(args.file)
|
||||
state = _load_state(state_path)
|
||||
expanded = set(state["expanded_seeds"])
|
||||
expanded.update(str(paper_id).strip() for paper_id in args.ids if str(paper_id).strip())
|
||||
state["expanded_seeds"] = sorted(expanded)
|
||||
_save_state(state_path, state)
|
||||
return {"expanded_seeds": state["expanded_seeds"]}
|
||||
|
||||
|
||||
def cmd_stats(args: argparse.Namespace) -> dict[str, Any]:
|
||||
state = _load_state(Path(args.file))
|
||||
papers = state["papers"]
|
||||
by_year: dict[str, int] = {}
|
||||
missing_title = 0
|
||||
for paper in papers.values():
|
||||
year = paper.get("year")
|
||||
key = str(year) if year else "unknown"
|
||||
by_year[key] = by_year.get(key, 0) + 1
|
||||
if not paper.get("title"):
|
||||
missing_title += 1
|
||||
return {
|
||||
"total": len(papers),
|
||||
"expanded_seeds": state["expanded_seeds"],
|
||||
"missing_title": missing_title,
|
||||
"by_year": dict(sorted(by_year.items(), reverse=True)),
|
||||
}
|
||||
|
||||
|
||||
def cmd_export(args: argparse.Namespace) -> dict[str, Any]:
|
||||
state = _load_state(Path(args.file))
|
||||
final = [
|
||||
{"id": paper_id, "title": paper.get("title", "")}
|
||||
for paper_id, paper in state["papers"].items()
|
||||
if paper.get("title")
|
||||
]
|
||||
out_path = Path(args.output)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with out_path.open("w", encoding="utf-8") as file:
|
||||
json.dump(final, file, ensure_ascii=False, indent=2)
|
||||
return {"count": len(final), "path": str(out_path)}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Deduplicated paper-set state file (stdout: JSON only, no network)."
|
||||
)
|
||||
parser.add_argument("--file", default=DEFAULT_STATE_FILE,
|
||||
help=f"State file path (default {DEFAULT_STATE_FILE}).")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
p_add = sub.add_parser("add", help="Merge papers from stdin JSON array or --ids.")
|
||||
p_add.add_argument("--ids", nargs="*", default=None,
|
||||
help="Add bare paper IDs instead of reading stdin.")
|
||||
p_add.set_defaults(func=cmd_add)
|
||||
|
||||
p_mark = sub.add_parser("mark-expanded", help="Record seed IDs whose references were expanded.")
|
||||
p_mark.add_argument("--ids", nargs="+", required=True)
|
||||
p_mark.set_defaults(func=cmd_mark_expanded)
|
||||
|
||||
p_stats = sub.add_parser("stats", help="Show totals, expanded seeds, and year distribution.")
|
||||
p_stats.set_defaults(func=cmd_stats)
|
||||
|
||||
p_export = sub.add_parser("export", help="Write the final [{id,title}] list.")
|
||||
p_export.add_argument("-o", "--output", default="outputs/final_papers.json")
|
||||
p_export.set_defaults(func=cmd_export)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
try:
|
||||
result = args.func(args)
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
print(json.dumps({"ok": False, "error": "invalid_input", "message": str(exc)},
|
||||
ensure_ascii=False))
|
||||
return 2
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SKILL_DIR = Path(__file__).resolve().parents[1]
|
||||
MODULE_PATH = SKILL_DIR / "scripts" / "aminer_api.py"
|
||||
SPEC = importlib.util.spec_from_file_location("aminer_deep_search_api", MODULE_PATH)
|
||||
assert SPEC and SPEC.loader
|
||||
api = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = api
|
||||
SPEC.loader.exec_module(api)
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, payload, status=200):
|
||||
self.payload = payload
|
||||
self.status = status
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
return False
|
||||
|
||||
def read(self):
|
||||
return json.dumps(self.payload).encode("utf-8")
|
||||
|
||||
|
||||
def run_main(argv, urlopen):
|
||||
"""Run the CLI with a fake transport; return (exit_code, stdout, stderr)."""
|
||||
stdout, stderr = io.StringIO(), io.StringIO()
|
||||
env = {"AMINER_API_KEY": "test-token"}
|
||||
api._call_counts.clear()
|
||||
with mock.patch.dict(os.environ, env, clear=True), \
|
||||
mock.patch.object(api.urllib.request, "urlopen", urlopen), \
|
||||
contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
||||
code = api.main(argv)
|
||||
return code, stdout.getvalue(), stderr.getvalue()
|
||||
|
||||
|
||||
def paper_info_response(request):
|
||||
body = json.loads(request.data.decode("utf-8"))
|
||||
return FakeResponse({
|
||||
"code": 200, "success": True,
|
||||
"data": [
|
||||
{"id": pid, "title": f"Title {pid}",
|
||||
"year": (2000 + int(pid)) if pid.isdigit() else 2015,
|
||||
"venue": {"raw": "Venue X"}, "abstract_slice": "abs " * 200}
|
||||
for pid in body["ids"]
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
class SearchTests(unittest.TestCase):
|
||||
def test_search_paginates_filters_year_and_enriches(self):
|
||||
search_pages = {
|
||||
0: [{"id": "1", "title": "P1"}, {"id": "2", "title": "P2"}] +
|
||||
[{"id": str(i), "title": f"P{i}"} for i in range(3, 21)],
|
||||
1: [{"id": "21", "title": "P21"}],
|
||||
}
|
||||
calls = {"search": [], "info": 0}
|
||||
|
||||
def fake_urlopen(request, timeout):
|
||||
url = request.full_url
|
||||
if "/api/paper/search/pro" in url:
|
||||
self.assertIn("keyword=graph", url)
|
||||
self.assertIn("order=n_citation", url)
|
||||
page = int(url.split("page=")[1].split("&")[0])
|
||||
calls["search"].append(page)
|
||||
return FakeResponse({"code": 200, "success": True,
|
||||
"data": search_pages.get(page, [])})
|
||||
if "/api/paper/info" in url:
|
||||
calls["info"] += 1
|
||||
return paper_info_response(request)
|
||||
raise AssertionError(f"unexpected url: {url}")
|
||||
|
||||
code, out, err = run_main(
|
||||
["search", "--query", "graph", "--size", "30",
|
||||
"--year", "2010", "--order", "n_citation", "--max-pages", "3"],
|
||||
fake_urlopen,
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
papers = json.loads(out)
|
||||
# info entries carry year 2000+id; the --year 2010 cutoff keeps ids 1..10 and none from page 1 (id 21)
|
||||
self.assertEqual([p["id"] for p in papers], [str(i) for i in range(1, 11)])
|
||||
self.assertEqual(papers[0]["venue"], "Venue X")
|
||||
self.assertLessEqual(len(papers[0]["abstract_slice"]), api.ABSTRACT_SLICE_LIMIT)
|
||||
self.assertEqual(calls["search"], [0, 1]) # stops after exhausting pages
|
||||
self.assertIn("[cost]", err)
|
||||
self.assertNotIn("[cost]", out) # stdout stays pure JSON
|
||||
|
||||
def test_missing_key_returns_structured_error(self):
|
||||
stdout = io.StringIO()
|
||||
api._call_counts.clear()
|
||||
with mock.patch.dict(os.environ, {}, clear=True), \
|
||||
contextlib.redirect_stdout(stdout):
|
||||
code = api.main(["search", "--query", "x"])
|
||||
self.assertEqual(code, 2)
|
||||
payload = json.loads(stdout.getvalue())
|
||||
self.assertEqual(payload["error"], "missing_aminer_api_key")
|
||||
self.assertNotIn("test-token", stdout.getvalue())
|
||||
|
||||
|
||||
class InfoTests(unittest.TestCase):
|
||||
def test_info_batches_at_most_100_ids(self):
|
||||
batch_sizes = []
|
||||
|
||||
def fake_urlopen(request, timeout):
|
||||
self.assertIn("/api/paper/info", request.full_url)
|
||||
body = json.loads(request.data.decode("utf-8"))
|
||||
batch_sizes.append(len(body["ids"]))
|
||||
return FakeResponse({"code": 200, "success": True,
|
||||
"data": [{"id": pid, "title": f"T{pid}"} for pid in body["ids"]]})
|
||||
|
||||
ids = [str(i) for i in range(150)]
|
||||
code, out, _ = run_main(["info", "--ids", *ids], fake_urlopen)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(batch_sizes, [100, 50])
|
||||
self.assertEqual(len(json.loads(out)), 150)
|
||||
|
||||
|
||||
class ReferencesTests(unittest.TestCase):
|
||||
def test_references_extracts_cited_excludes_seeds_and_tracks_sources(self):
|
||||
def fake_urlopen(request, timeout):
|
||||
url = request.full_url
|
||||
if "/api/paper/relation" in url:
|
||||
seed = url.split("id=")[1]
|
||||
cited = {
|
||||
"s1": [{"_id": "a"}, {"_id": "b"}, {"_id": "s2"}],
|
||||
"s2": [{"_id": "b"}, {"_id": "c"}],
|
||||
}[seed]
|
||||
return FakeResponse({"code": 200, "success": True,
|
||||
"data": [{"_id": seed, "cited": cited}]})
|
||||
if "/api/paper/info" in url:
|
||||
return paper_info_response(request)
|
||||
raise AssertionError(f"unexpected url: {url}")
|
||||
|
||||
code, out, _ = run_main(["references", "--ids", "s1", "s2"], fake_urlopen)
|
||||
self.assertEqual(code, 0)
|
||||
papers = {p["id"]: p for p in json.loads(out)}
|
||||
self.assertEqual(set(papers), {"a", "b", "c"}) # seed s2 excluded
|
||||
self.assertEqual(papers["a"]["source_paper_ids"], ["s1"])
|
||||
self.assertEqual(papers["b"]["source_paper_ids"], ["s1", "s2"])
|
||||
|
||||
def test_per_seed_limit(self):
|
||||
def fake_urlopen(request, timeout):
|
||||
url = request.full_url
|
||||
if "/api/paper/relation" in url:
|
||||
cited = [{"_id": f"r{i}"} for i in range(30)]
|
||||
return FakeResponse({"code": 200, "success": True,
|
||||
"data": [{"_id": "s1", "cited": cited}]})
|
||||
if "/api/paper/info" in url:
|
||||
return paper_info_response(request)
|
||||
raise AssertionError(f"unexpected url: {url}")
|
||||
|
||||
code, out, _ = run_main(["references", "--ids", "s1", "--per-seed", "5"], fake_urlopen)
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(len(json.loads(out)), 5)
|
||||
|
||||
|
||||
class QaSearchTests(unittest.TestCase):
|
||||
def test_qa_search_always_uses_topic_mode(self):
|
||||
captured = {}
|
||||
|
||||
def fake_urlopen(request, timeout):
|
||||
url = request.full_url
|
||||
if "/api/paper/qa/search" in url:
|
||||
captured["body"] = json.loads(request.data.decode("utf-8"))
|
||||
return FakeResponse({"code": 200, "success": True,
|
||||
"data": [{"id": "1", "title": "P1"}]})
|
||||
if "/api/paper/info" in url:
|
||||
return paper_info_response(request)
|
||||
raise AssertionError(f"unexpected url: {url}")
|
||||
|
||||
code, out, _ = run_main(
|
||||
["qa-search", "--query", "GNN scalability",
|
||||
"--topic-high", '[["graph neural network"]]', "--size", "5"],
|
||||
fake_urlopen,
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
# with use_topic=false the backend ignores `query` (only reads `title`),
|
||||
# yielding empty terms and envelope 403 — so the script must always send true
|
||||
self.assertIs(captured["body"]["use_topic"], True)
|
||||
self.assertEqual(captured["body"]["query"], "GNN scalability")
|
||||
self.assertEqual(captured["body"]["topic_high"], '[["graph neural network"]]')
|
||||
self.assertEqual(json.loads(out)[0]["id"], "1")
|
||||
|
||||
def test_qa_search_requires_query_or_topic(self):
|
||||
code, out, _ = run_main(["qa-search"], lambda *a, **k: None)
|
||||
self.assertEqual(code, 2)
|
||||
self.assertEqual(json.loads(out)["error"], "invalid_request")
|
||||
|
||||
|
||||
class TransportTests(unittest.TestCase):
|
||||
def test_http_200_with_error_code_in_envelope_is_reported(self):
|
||||
def fake_urlopen(request, timeout):
|
||||
return FakeResponse({"code": 403, "success": True, "msg": "no data", "data": None})
|
||||
|
||||
code, out, _ = run_main(
|
||||
["qa-search", "--query", "scalable GNN approaches"], fake_urlopen,
|
||||
)
|
||||
self.assertEqual(code, 1)
|
||||
payload = json.loads(out)
|
||||
self.assertEqual(payload["error"], "api_error")
|
||||
self.assertEqual(payload["code"], 403)
|
||||
|
||||
|
||||
def test_non_retryable_http_error_fails_fast(self):
|
||||
attempts = []
|
||||
|
||||
def fake_urlopen(request, timeout):
|
||||
attempts.append(1)
|
||||
raise api.urllib.error.HTTPError(
|
||||
request.full_url, 401, "Unauthorized", {},
|
||||
io.BytesIO(b'{"msg":"bad token"}'),
|
||||
)
|
||||
|
||||
code, out, _ = run_main(["info", "--ids", "x"], fake_urlopen)
|
||||
self.assertEqual(code, 1)
|
||||
payload = json.loads(out)
|
||||
self.assertEqual(payload["status"], 401)
|
||||
self.assertEqual(len(attempts), 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SKILL_DIR = Path(__file__).resolve().parents[1]
|
||||
MODULE_PATH = SKILL_DIR / "scripts" / "paper_set.py"
|
||||
SPEC = importlib.util.spec_from_file_location("aminer_deep_search_paper_set", MODULE_PATH)
|
||||
assert SPEC and SPEC.loader
|
||||
paper_set = importlib.util.module_from_spec(SPEC)
|
||||
sys.modules[SPEC.name] = paper_set
|
||||
SPEC.loader.exec_module(paper_set)
|
||||
|
||||
|
||||
def run_main(argv, stdin_text=None):
|
||||
stdout = io.StringIO()
|
||||
with contextlib.redirect_stdout(stdout):
|
||||
if stdin_text is not None:
|
||||
with mock.patch.object(sys, "stdin", io.StringIO(stdin_text)):
|
||||
code = paper_set.main(argv)
|
||||
else:
|
||||
code = paper_set.main(argv)
|
||||
return code, stdout.getvalue()
|
||||
|
||||
|
||||
class PaperSetTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.state = str(Path(self.tmp.name) / "paper_set.json")
|
||||
|
||||
def tearDown(self):
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_add_from_stdin_dedupes_and_counts(self):
|
||||
items = [
|
||||
{"id": "a", "title": "Paper A", "year": 2020},
|
||||
{"id": "b", "title": "Paper B"},
|
||||
]
|
||||
code, out = run_main(["--file", self.state, "add"], json.dumps(items))
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(json.loads(out), {"added": 2, "duplicates": 0, "total": 2})
|
||||
|
||||
again = [{"id": "a", "title": "Paper A"}, {"id": "c", "title": "Paper C"}]
|
||||
code, out = run_main(["--file", self.state, "add"], json.dumps(again))
|
||||
self.assertEqual(json.loads(out), {"added": 1, "duplicates": 1, "total": 3})
|
||||
|
||||
def test_add_bare_ids(self):
|
||||
code, out = run_main(["--file", self.state, "add", "--ids", "x", "y", "x"])
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(json.loads(out), {"added": 2, "duplicates": 1, "total": 2})
|
||||
|
||||
def test_source_paper_ids_mark_seeds_expanded(self):
|
||||
items = [{"id": "a", "title": "A", "source_paper_ids": ["seed1", "seed2"]}]
|
||||
run_main(["--file", self.state, "add"], json.dumps(items))
|
||||
code, out = run_main(["--file", self.state, "stats"])
|
||||
stats = json.loads(out)
|
||||
self.assertEqual(stats["expanded_seeds"], ["seed1", "seed2"])
|
||||
# source_paper_ids must not be persisted onto the paper entry
|
||||
state = json.loads(Path(self.state).read_text(encoding="utf-8"))
|
||||
self.assertNotIn("source_paper_ids", state["papers"]["a"])
|
||||
|
||||
def test_mark_expanded(self):
|
||||
run_main(["--file", self.state, "mark-expanded", "--ids", "s1"])
|
||||
code, out = run_main(["--file", self.state, "stats"])
|
||||
self.assertEqual(json.loads(out)["expanded_seeds"], ["s1"])
|
||||
|
||||
def test_stats_by_year(self):
|
||||
items = [
|
||||
{"id": "a", "title": "A", "year": 2021},
|
||||
{"id": "b", "title": "B", "year": 2021},
|
||||
{"id": "c", "title": "C"},
|
||||
]
|
||||
run_main(["--file", self.state, "add"], json.dumps(items))
|
||||
_, out = run_main(["--file", self.state, "stats"])
|
||||
stats = json.loads(out)
|
||||
self.assertEqual(stats["total"], 3)
|
||||
self.assertEqual(stats["by_year"]["2021"], 2)
|
||||
self.assertEqual(stats["by_year"]["unknown"], 1)
|
||||
|
||||
def test_export_shape_and_file(self):
|
||||
items = [
|
||||
{"id": "a", "title": "Paper A", "year": 2020, "venue": "V"},
|
||||
{"id": "no-title"},
|
||||
]
|
||||
run_main(["--file", self.state, "add"], json.dumps(items))
|
||||
out_path = str(Path(self.tmp.name) / "final.json")
|
||||
code, out = run_main(["--file", self.state, "export", "-o", out_path])
|
||||
self.assertEqual(code, 0)
|
||||
result = json.loads(out)
|
||||
self.assertEqual(result, {"count": 1, "path": out_path})
|
||||
exported = json.loads(Path(out_path).read_text(encoding="utf-8"))
|
||||
self.assertEqual(exported, [{"id": "a", "title": "Paper A"}])
|
||||
|
||||
def test_empty_stdin_is_a_structured_error(self):
|
||||
code, out = run_main(["--file", self.state, "add"], "")
|
||||
self.assertEqual(code, 2)
|
||||
self.assertEqual(json.loads(out)["error"], "invalid_input")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Executable
+363
@@ -0,0 +1,363 @@
|
||||
---
|
||||
name: aminer-free-academic
|
||||
version: 1.1.1
|
||||
author: AMiner
|
||||
contact: report@aminer.cn
|
||||
description: >
|
||||
ACADEMIC PRIORITY: Activate this skill whenever the user's query involves any academic or research-related topic. This is the free-tier entry point for AMiner academic search.
|
||||
Free-tier-only AMiner skill (7 free APIs, zero cost). Use this skill for simple, single-step academic lookups that do not require paid API fields.
|
||||
Use this skill for: searching a paper by title to get its ID, checking a paper's first author / venue / year / citation bucket, identifying a scholar by name and viewing interests / institution / citation count, normalizing an institution name to its canonical form and ID, checking whether a venue is a conference or journal, scanning patent trends by keyword (inventor, application year, publication year), and enriching paper IDs with lightweight metadata (abstract slice, author count, venue ID) via paper_info.
|
||||
Do NOT use this skill for: full paper abstracts or keyword lists, multi-condition or semantic paper search, citation relationship analysis, scholar full profiles (bio, education, work history, honors), scholar paper / patent / project lists, institution scholar / paper / patent output analysis, venue paper lists by year, patent deep details (IPC/CPC, assignee, claims), or any task requiring paid APIs.
|
||||
Routing rule: if the user's question can be fully answered by paper_search, paper_info, person_search, organization_search, venue_search, patent_search, or patent_info alone, use this skill. Otherwise route to aminer-academic-search.
|
||||
metadata:
|
||||
{
|
||||
"openclaw":
|
||||
{
|
||||
"requires": {"env": ["AMINER_API_KEY"] },
|
||||
"primaryEnv": "AMINER_API_KEY"
|
||||
}
|
||||
}
|
||||
---
|
||||
|
||||
# AMiner Free Search
|
||||
|
||||
Use this skill for AMiner requests that should stay on the free tier first. It is designed for discovery, initial screening, and entity normalization, not deep analysis.
|
||||
|
||||
## Scope
|
||||
|
||||
This skill uses only the upgraded free interfaces:
|
||||
|
||||
- `paper_search`
|
||||
- `paper_info`
|
||||
- `person_search`
|
||||
- `organization_search`
|
||||
- `venue_search`
|
||||
- `patent_search`
|
||||
- `patent_info`
|
||||
|
||||
Current free-tier fields emphasized by this skill:
|
||||
|
||||
- `paper_search`: `venue_name`, `first_author`, `n_citation_bucket`, `year`
|
||||
- `paper_info`: `abstract_slice`, `year`, `venue_id`, `author_count`
|
||||
- `organization_search`: `aliases` (top 3)
|
||||
- `venue_search`: `aliases` (top 3), `venue_type`
|
||||
- `patent_search`: `inventor_name` (first), `app_year`, `pub_year`
|
||||
- `patent_info`: `app_year`, `pub_year`
|
||||
- `person_search`: `interests`, `n_citation`, institution fields
|
||||
|
||||
## Primary Goal
|
||||
|
||||
Use free APIs to help the user answer:
|
||||
|
||||
- What is this entity?
|
||||
- Is it relevant enough to continue?
|
||||
- Which candidate should I inspect next?
|
||||
- Can I normalize this institution or venue name?
|
||||
- Is there enough value to justify upgrading to paid APIs?
|
||||
|
||||
Do not use this skill for full scholar portraits, citation-chain analysis, full-text-like paper understanding, large-scale monitoring, or institution output analysis.
|
||||
|
||||
## Mandatory Rules
|
||||
|
||||
1. Stay on free APIs unless the user explicitly asks to upgrade or the free path clearly cannot answer the question.
|
||||
2. Be explicit about free-tier limits. Say what can be answered now and what would require a paid upgrade.
|
||||
3. Use free results to narrow candidates before suggesting any paid API.
|
||||
4. If returning entities, append AMiner URLs when IDs are available:
|
||||
- Paper: `https://www.aminer.cn/pub/{paper_id}`
|
||||
- Scholar: `https://www.aminer.cn/profile/{scholar_id}`
|
||||
- Patent: `https://www.aminer.cn/patent/{patent_id}`
|
||||
- Venue: `https://www.aminer.cn/open/journal/detail/{venue_id}`
|
||||
|
||||
## Token Check (Required)
|
||||
|
||||
Before making any API call, verify that the environment variable `AMINER_API_KEY` exists. Never output the token in plain text.
|
||||
|
||||
```bash
|
||||
if [ -z "${AMINER_API_KEY+x}" ]; then
|
||||
echo "AMINER_API_KEY does not exist"
|
||||
else
|
||||
echo "AMINER_API_KEY exists"
|
||||
fi
|
||||
```
|
||||
|
||||
- If `${AMINER_API_KEY}` exists: proceed with the query.
|
||||
- If `${AMINER_API_KEY}` is not set: stop immediately and guide the user to the [AMiner Console](https://open.aminer.cn/open/board?tab=control) to generate one. For help, see the [Open Platform Documentation](https://open.aminer.cn/open/docs).
|
||||
- If the user provides `AMINER_API_KEY` inline (e.g. "My token is xxx"), accept it for the current session, but recommend setting it as an environment variable for better security.
|
||||
|
||||
## Invocation Style
|
||||
|
||||
Use direct `curl` calls by default. A Python wrapper is not required for this skill.
|
||||
|
||||
Default headers:
|
||||
|
||||
- `Authorization: ${AMINER_API_KEY}` by default
|
||||
- `Content-Type: application/json;charset=utf-8` for POST requests
|
||||
- `X-Platform: openclaw` when required by the gateway
|
||||
|
||||
## When To Use
|
||||
|
||||
Use this skill when the user asks for:
|
||||
|
||||
- free AMiner search
|
||||
- low-cost academic discovery
|
||||
- paper screening
|
||||
- scholar identification
|
||||
- institution normalization
|
||||
- venue normalization
|
||||
- patent trend scanning
|
||||
- representative results before deeper analysis
|
||||
|
||||
Trigger phrases include:
|
||||
|
||||
- “先用免费接口”
|
||||
- “不要走收费接口”
|
||||
- “先帮我筛一下”
|
||||
- “先看看值不值得深挖”
|
||||
- “找几个候选”
|
||||
- “做一个轻量版 skill”
|
||||
|
||||
## Free Workflows
|
||||
|
||||
### 1. Paper triage
|
||||
|
||||
Use when the user wants to quickly judge whether a paper is relevant.
|
||||
|
||||
Default chain:
|
||||
|
||||
`paper_search -> paper_info`
|
||||
|
||||
Return:
|
||||
|
||||
- title
|
||||
- first author
|
||||
- venue name
|
||||
- year
|
||||
- citation bucket
|
||||
- abstract slice
|
||||
- paper URL
|
||||
|
||||
This can answer:
|
||||
|
||||
- Is this probably the right paper?
|
||||
- Is it recent?
|
||||
- Is it from a recognizable venue?
|
||||
- Is it worth opening in detail?
|
||||
|
||||
### 2. Scholar identification
|
||||
|
||||
Use when the user wants to know which scholar is the right person.
|
||||
|
||||
Default chain:
|
||||
|
||||
`person_search`
|
||||
|
||||
Return:
|
||||
|
||||
- name
|
||||
- org
|
||||
- interests
|
||||
- citation count
|
||||
- scholar URL
|
||||
|
||||
This can answer:
|
||||
|
||||
- Is this the right scholar?
|
||||
- What interests best describe this person?
|
||||
- Which institution candidate is the best match?
|
||||
|
||||
### 3. Institution normalization
|
||||
|
||||
Use when the user provides an institution string or abbreviation.
|
||||
|
||||
Default chain:
|
||||
|
||||
`organization_search`
|
||||
|
||||
Return:
|
||||
|
||||
- org id
|
||||
- standard name
|
||||
- aliases (top 3)
|
||||
|
||||
This can answer:
|
||||
|
||||
- Is this institution name recognized?
|
||||
- Which canonical organization should downstream workflows use?
|
||||
|
||||
### 4. Venue normalization and type check
|
||||
|
||||
Use when the user provides a conference or journal name.
|
||||
|
||||
Default chain:
|
||||
|
||||
`venue_search`
|
||||
|
||||
Return:
|
||||
|
||||
- venue id
|
||||
- standard bilingual name
|
||||
- aliases (top 3)
|
||||
- venue type
|
||||
- venue URL
|
||||
|
||||
This can answer:
|
||||
|
||||
- Is this a conference or a journal?
|
||||
- What is the standard venue entity?
|
||||
|
||||
### 5. Patent trend scan
|
||||
|
||||
Use when the user wants a lightweight view of patents in a topic.
|
||||
|
||||
Default chain:
|
||||
|
||||
`patent_search -> patent_info` when IDs need basic enrichment
|
||||
|
||||
Return:
|
||||
|
||||
- patent title
|
||||
- first inventor
|
||||
- app year
|
||||
- pub year
|
||||
- patent number and country when `patent_info` is added
|
||||
- patent URL
|
||||
|
||||
This can answer:
|
||||
|
||||
- Is the topic active recently?
|
||||
- Who appears first in the inventor field?
|
||||
- Is there recent patent activity worth deeper review?
|
||||
|
||||
### 6. Free entity map
|
||||
|
||||
Use when the user wants a quick map of a topic across papers, scholars, venues, institutions, and patents without paying for analysis-grade APIs.
|
||||
|
||||
Suggested chain:
|
||||
|
||||
- papers: `paper_search -> paper_info`
|
||||
- scholars: `person_search`
|
||||
- institutions: `organization_search`
|
||||
- venues: `venue_search`
|
||||
- patents: `patent_search -> patent_info`
|
||||
|
||||
Return a short cross-entity summary, not a deep report.
|
||||
|
||||
## Free Skill Examples
|
||||
|
||||
### 1. Paper triage
|
||||
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/paper/search?page=1&size=5&title=Attention%20Is%20All%20You%20Need' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw'
|
||||
```
|
||||
|
||||
Then enrich with `paper_info`:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/paper/info' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"ids":["<PAPER_ID>"]}'
|
||||
```
|
||||
|
||||
### 2. Scholar identification
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/person/search' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"name":"Yann LeCun","size":5}'
|
||||
```
|
||||
|
||||
### 3. Institution normalization
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/organization/search' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"orgs":["MIT CSAIL"]}'
|
||||
```
|
||||
|
||||
### 4. Venue normalization and type check
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/venue/search' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"name":"tkde"}'
|
||||
```
|
||||
|
||||
### 5. Patent trend scan
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/patent/search' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"query":"quantum computing chip","page":0,"size":10}'
|
||||
```
|
||||
|
||||
## Output Pattern
|
||||
|
||||
Prefer this structure:
|
||||
|
||||
```markdown
|
||||
## Free-tier result
|
||||
|
||||
### What we can answer now
|
||||
- ...
|
||||
|
||||
### Top candidates
|
||||
- ...
|
||||
|
||||
### Suggested next step
|
||||
- Stay free: ...
|
||||
- Upgrade to paid API only if you need: ...
|
||||
```
|
||||
|
||||
## Paid Upgrade Boundary
|
||||
|
||||
Recommend upgrading only when the user needs one of these:
|
||||
|
||||
- full abstract or full paper metadata
|
||||
- multi-condition or semantic paper search
|
||||
- citation relationships
|
||||
- full scholar profile, works, patents, or projects
|
||||
- institution scholars, papers, patents, or rich profiles
|
||||
- venue paper lists by year
|
||||
- full patent details such as IPC/CPC, assignee, description
|
||||
|
||||
Suggested paid handoff:
|
||||
|
||||
- deeper paper analysis: `paper_search_pro`, `paper_detail`, `paper_relation`
|
||||
- deeper scholar analysis: `person/detail`, `person/figure`, `person/paper/relation`
|
||||
- deeper org analysis: `organization/detail`, `organization/person/relation`, `organization/paper/relation`
|
||||
- deeper venue analysis: `venue/detail`, `venue/paper/relation`
|
||||
- deeper patent analysis: `patent/detail`
|
||||
|
||||
## Product Positioning
|
||||
|
||||
This skill is intentionally positioned for:
|
||||
|
||||
- first success
|
||||
- free discovery
|
||||
- candidate narrowing
|
||||
- entity normalization
|
||||
- upgrade qualification
|
||||
|
||||
It should not replace the paid skill. It should create demand for it.
|
||||
|
||||
## Additional Reference
|
||||
|
||||
For endpoint parameters and fields, read [references/api-catalog.md](references/api-catalog.md).
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"skill_name": "aminer-free-academic",
|
||||
"evals": [
|
||||
{
|
||||
"id": 1,
|
||||
"prompt": "Help me quickly check whether the paper 'Attention Is All You Need' is worth a deep dive. Use only free AMiner APIs. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the paper triage workflow: paper_search to locate the paper, then paper_info to enrich with abstract slice, year, venue, and author count. Output a lightweight card with a paper URL.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called Paper Search API (paper_search) to find the paper by title",
|
||||
"Called Paper Info API (paper_info) with the returned paper ID",
|
||||
"Output includes title, first_author, venue_name, year, and n_citation_bucket",
|
||||
"Output includes abstract_slice from paper_info",
|
||||
"Output includes a paper URL in the format https://www.aminer.cn/pub/{paper_id}",
|
||||
"No paid API was called"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"prompt": "I want to identify which 'Andrew Ng' on AMiner is the Stanford professor. Use only free APIs. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the scholar identification workflow: person_search with name and optional org filter. Return candidate list with interests, org, and citation count to help the user pick the right one.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called Scholar Search API (person_search) with name 'Andrew Ng'",
|
||||
"Output includes multiple candidates with name, org, interests, and n_citation",
|
||||
"Output includes scholar URLs in the format https://www.aminer.cn/profile/{scholar_id}",
|
||||
"No paid API was called"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"prompt": "Normalize the institution name 'MIT CSAIL' using AMiner free APIs. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the institution normalization workflow: organization_search with the raw institution string. Return the canonical org name, org_id, and aliases.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called Org Search API (organization_search) with orgs=['MIT CSAIL']",
|
||||
"Output includes org_id and standardized org_name",
|
||||
"Output includes aliases list",
|
||||
"No paid API was called"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"prompt": "Help me look up recent patents about quantum computing",
|
||||
"expected_output": "Detect that the user has not provided a token; immediately stop all subsequent API calls and guide the user to obtain a token from the AMiner Console.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Clearly inform the user that a token is currently missing and AMiner API calls cannot continue",
|
||||
"No API calls were made",
|
||||
"Provide the console link https://open.aminer.cn/open/board?tab=control",
|
||||
"Prompt the user to continue after obtaining a token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"prompt": "Is 'TKDE' a conference or a journal? Help me normalize it using free AMiner APIs. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the venue normalization workflow: venue_search with the name string. Return the standard bilingual name, venue_type, aliases, and venue URL.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called Venue Search API (venue_search) with name 'TKDE'",
|
||||
"Output includes venue_id, standard name (English and/or Chinese), and venue_type",
|
||||
"Output includes aliases list",
|
||||
"Output includes a venue URL in the format https://www.aminer.cn/open/journal/detail/{venue_id}",
|
||||
"No paid API was called"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"prompt": "Scan recent patent activity in the field of 'autonomous driving lidar' using free APIs only. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the patent trend scan workflow: patent_search to find patents, optionally enriched by patent_info. Return patent titles, first inventor, app_year, pub_year, and patent URLs.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called Patent Search API (patent_search) with a query about autonomous driving lidar",
|
||||
"Output includes patent titles, inventor_name, app_year, and pub_year",
|
||||
"Output includes patent URLs in the format https://www.aminer.cn/patent/{patent_id}",
|
||||
"No paid API was called"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"prompt": "Give me a quick free-tier overview of the topic 'graph neural network' — papers, scholars, institutions, and venues. My token is <YOUR_TOKEN>",
|
||||
"expected_output": "Run the free entity map workflow: combine paper_search, person_search, organization_search, and venue_search to build a lightweight cross-entity summary.",
|
||||
"files": [],
|
||||
"expectations": [
|
||||
"Called at least paper_search and person_search",
|
||||
"Output covers multiple entity types (papers, scholars, and optionally institutions/venues)",
|
||||
"Output includes AMiner URLs for returned entities",
|
||||
"Clearly states what free-tier can answer and what would require paid upgrade",
|
||||
"No paid API was called"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
# AMiner Free Search API Catalog
|
||||
|
||||
**Base URL**: `https://publicapi.chatglm.cn/chatglm_public/skill/aminer`
|
||||
**Authentication**: All endpoints should default to `Authorization: ${AMINER_API_KEY}`. In workflow execution, also include `X-Platform: openclaw` when required by the gateway.
|
||||
**Scope**: This catalog only documents the free APIs used by `aminer-free-academic`.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Paper APIs](#paper-apis)
|
||||
- [Scholar APIs](#scholar-apis)
|
||||
- [Institution APIs](#institution-apis)
|
||||
- [Venue APIs](#venue-apis)
|
||||
- [Patent APIs](#patent-apis)
|
||||
|
||||
---
|
||||
|
||||
## Paper APIs
|
||||
|
||||
### 1. Paper Search
|
||||
|
||||
- **URL**: `GET /api/paper/search`
|
||||
- **Price**: Free
|
||||
- **Description**: Search papers by title and return low-cost screening fields suitable for fast paper triage.
|
||||
|
||||
**Request Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|--------|------|------|------|
|
||||
| page | number | Yes | Page number. Current online definition says it starts from `1`. |
|
||||
| size | number | No | Page size, maximum `20` |
|
||||
| title | string | Yes | Paper title |
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Field | Description |
|
||||
|--------|------|
|
||||
| id | Paper ID |
|
||||
| title | Paper title |
|
||||
| title_zh | Paper title in Chinese |
|
||||
| doi | DOI |
|
||||
| first_author | First author |
|
||||
| venue_name | Venue title |
|
||||
| n_citation_bucket | Citation bucket: `0`, `1-10`, `11-50`, `51-200`, `200-1000`, `1000-5000`, `5000+` |
|
||||
| year | Publication year |
|
||||
| total | Total count |
|
||||
|
||||
**curl Example:**
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/paper/search?page=1&size=10&title=Looking+at+CTR+Prediction+Again%3A+Is+Attention+All+You+Need' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Paper Info
|
||||
|
||||
- **URL**: `POST /api/paper/info`
|
||||
- **Price**: Free
|
||||
- **Description**: Batch query paper basic cards by paper IDs. Suitable for enriching search results with lightweight metadata.
|
||||
|
||||
**Request Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|--------|------|------|------|
|
||||
| ids | []string | Yes | Paper ID list, maximum `100` |
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Field | Description |
|
||||
|--------|------|
|
||||
| id | Paper ID |
|
||||
| title | Paper title |
|
||||
| abstract_slice | Partial abstract |
|
||||
| authors | Author array |
|
||||
| author_count | Total author count |
|
||||
| issue | Volume / issue field |
|
||||
| raw | Venue raw name |
|
||||
| venue | Venue object |
|
||||
| venue_id | Venue ID |
|
||||
| year | Publication year |
|
||||
|
||||
**curl Example:**
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/paper/info' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"ids":["5ce2c5a5ced107d4c61c839b"]}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scholar APIs
|
||||
|
||||
### 3. Scholar Search
|
||||
|
||||
- **URL**: `POST /api/person/search`
|
||||
- **Price**: Free
|
||||
- **Description**: Search scholar candidates by name and optional institution condition.
|
||||
|
||||
**Request Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|--------|------|------|------|
|
||||
| name | string | No | Scholar name |
|
||||
| offset | number | No | Starting position (fixed at 0; pagination not supported) |
|
||||
| org | string | No | Institution name |
|
||||
| size | number | No | Number of results, maximum `10` |
|
||||
| org_id | []string | No | Institution entity ID list |
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Field | Description |
|
||||
|--------|------|
|
||||
| id | Scholar ID |
|
||||
| interests | Research interests |
|
||||
| n_citation | Citation count |
|
||||
| name | Name |
|
||||
| name_zh | Chinese name |
|
||||
| org | Institution in English |
|
||||
| org_id | Institution ID |
|
||||
| org_zh | Institution in Chinese |
|
||||
| total | Total count |
|
||||
|
||||
**curl Example:**
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/person/search' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"name":"王曙","offset":0,"org":"Shanghai Jiaotong","size":10}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Institution APIs
|
||||
|
||||
### 4. Org Search
|
||||
|
||||
- **URL**: `POST /api/organization/search`
|
||||
- **Price**: Free
|
||||
- **Description**: Search institution IDs and standard names from institution keywords.
|
||||
|
||||
**Request Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|--------|------|------|------|
|
||||
| orgs | []string | No | Institution names |
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Field | Description |
|
||||
|--------|------|
|
||||
| aliases | Alias list, partial and typically top 3 |
|
||||
| org_id | Institution ID |
|
||||
| org_name | Institution name |
|
||||
| total | Total count |
|
||||
|
||||
**curl Example:**
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/organization/search' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"orgs":["清华大学"]}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Venue APIs
|
||||
|
||||
### 5. Venue Search
|
||||
|
||||
- **URL**: `POST /api/venue/search`
|
||||
- **Price**: Free
|
||||
- **Description**: Search venue IDs and standard names by venue keyword, including aliases and venue type.
|
||||
|
||||
**Request Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|--------|------|------|------|
|
||||
| name | string | No | Venue name |
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Field | Description |
|
||||
|--------|------|
|
||||
| id | Venue ID |
|
||||
| name_en | Venue English name |
|
||||
| name_zh | Venue Chinese name |
|
||||
| aliases | Alias list, partial and typically top 3 |
|
||||
| venue_type | Venue type: `journal` or `conference` |
|
||||
| total | Total count |
|
||||
|
||||
**curl Example:**
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/venue/search' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"name":"tkde"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Patent APIs
|
||||
|
||||
### 6. Patent Search
|
||||
|
||||
- **URL**: `POST /api/patent/search`
|
||||
- **Price**: Free
|
||||
- **Description**: Search patents by title or keyword and return lightweight trend fields.
|
||||
|
||||
**Request Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|--------|------|------|------|
|
||||
| query | string | Yes | Query text such as patent title or keywords |
|
||||
| page | number | Yes | Page number |
|
||||
| size | number | Yes | Page size |
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Field | Description |
|
||||
|--------|------|
|
||||
| id | Patent ID |
|
||||
| title | Patent title in English |
|
||||
| title_zh | Patent title in Chinese |
|
||||
| inventor_name | First inventor name |
|
||||
| app_year | Application year |
|
||||
| pub_year | Publication year |
|
||||
|
||||
**curl Example:**
|
||||
```bash
|
||||
curl -X POST \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/patent/search' \
|
||||
-H 'Content-Type: application/json;charset=utf-8' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw' \
|
||||
-d '{"page":0,"query":"Si02","size":20}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. Patent Info
|
||||
|
||||
- **URL**: `GET /api/patent/info`
|
||||
- **Price**: Free
|
||||
- **Description**: Retrieve a patent basic card by patent ID.
|
||||
|
||||
**Request Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|--------|------|------|------|
|
||||
| id | string | Yes | Patent ID |
|
||||
|
||||
**Response Fields:**
|
||||
|
||||
| Field | Description |
|
||||
|--------|------|
|
||||
| id | Patent ID |
|
||||
| title / en | Patent title |
|
||||
| app_num | Application number |
|
||||
| pub_num | Publication number |
|
||||
| pub_kind | Publication kind |
|
||||
| inventor | Inventor |
|
||||
| country | Country |
|
||||
| sequence | Sequence |
|
||||
| app_year | Application year |
|
||||
| pub_year | Publication year |
|
||||
|
||||
**curl Example:**
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://publicapi.chatglm.cn/chatglm_public/skill/aminer/api/patent/info?id=<PATENT_ID>' \
|
||||
-H "Authorization: ${AMINER_API_KEY}" \
|
||||
-H 'X-Platform: openclaw'
|
||||
```
|
||||
Executable
+245
@@ -0,0 +1,245 @@
|
||||
---
|
||||
name: anti-pua
|
||||
description: 识别和分析PUA(Pickup Artist)及情感操纵行为的专业心理分析工具。具备人格分析、心理侧写、情感分析能力,能够识别情感操纵、煤气灯操纵、虐待等有毒关系模式,评估人格特质(如黑暗三人格、脆弱型自恋等),预测对方行为并给出具体的相处建议。当用户需要:分析对方言行动机、识别PUA/情感操纵行为、评估NPD(自恋型人格障碍)倾向、识别操纵行为、预测对方未来行为、寻求健康关系建议、分析黑暗三人格或光明三人格时使用此skill。
|
||||
---
|
||||
|
||||
# 反PUA大师 - 情感操纵识别与心理分析
|
||||
|
||||
## 核心角色定位
|
||||
|
||||
你是一位专业行为分析专家和心理顾问。你具备:
|
||||
|
||||
- **人格分析能力**:深度分析人格特质,识别黑暗三人格(自恋、马基雅维利主义、精神病态)、脆弱型自恋、光明三人格等
|
||||
- **心理侧写能力**:通过言行分析对方的心理动机和内在模式
|
||||
- **情感分析能力**:识别情感操纵、虐待、煤气灯操纵(gaslighting)等具体行为
|
||||
- **行为预测能力**:基于人格分析预测对方下一步可能的行为
|
||||
|
||||
## 🚀 使用开始 - 必须先询问的问题
|
||||
|
||||
**当用户开始使用这个skill时,必须先按顺序询问以下问题,收集关键信息:**
|
||||
|
||||
### 问题1:对方与咨询人的关系
|
||||
"请问对方与您是什么关系?"
|
||||
- 朋友、情侣、配偶
|
||||
- 领导、同事、下属
|
||||
- 导师、老师、同学
|
||||
- 父母、兄妹、姐弟、子女、亲戚
|
||||
- 陌生人、保密、其他
|
||||
|
||||
### 问题2:用户希望我们做什么
|
||||
"根据对方目前的行为表现,您希望我们为您做什么?"
|
||||
- 分析行为动机
|
||||
- 识别PUA
|
||||
- 评估NPD(自恋型人格障碍)倾向
|
||||
- 识别操纵行为
|
||||
- 预测未来行为
|
||||
- 倾听疗愈
|
||||
- 寻求健康关系建议
|
||||
- 黑暗三人格分析
|
||||
- 光明三人格分析
|
||||
- 其他(请说明)
|
||||
|
||||
### 问题3:提供详细描述
|
||||
"请客观详细描述对方在与您相处时的言行细节,包括:"
|
||||
- 具体的对话内容(原话或近似表述)
|
||||
- 具体的行为表现
|
||||
- 这些言行发生的情境和背景
|
||||
- 您当时的感受和反应
|
||||
|
||||
### 问题4:支持上传材料
|
||||
"您也可以上传以下材料辅助分析:"
|
||||
- 聊天记录截图
|
||||
- 对话文字记录
|
||||
- 其他相关证据
|
||||
|
||||
### 问题5:概念咨询(可选)
|
||||
"或者,如果您只是想了解与PUA相关的概念,也可以随时问我,例如:"
|
||||
- 什么是PUA?
|
||||
- 什么是NPD?
|
||||
- 什么是煤气灯操纵?
|
||||
- 什么是爱情轰炸?
|
||||
- 其他PUA相关概念
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
**在收集完上述信息后,按照以下流程进行分析:**
|
||||
|
||||
**特殊处理场景:**
|
||||
|
||||
1. **概念咨询**:如果用户选择了解PUA相关概念(问题5),直接以通俗易懂的方式解释相关概念,配合实际案例说明,不需要走完整分析流程。
|
||||
|
||||
2. **倾听疗愈**:如果用户选择"倾听疗愈"(问题2),优先表达共情和理解,不急于分析,避免结构化输出,以对话式回应为主。
|
||||
|
||||
3. **信息不足处理**:如果用户提供的描述过于简略或模糊,应先追问具体细节(如具体对话内容、行为情境、发生时间等),不要基于有限信息做出过度推断。
|
||||
|
||||
4. **反向利用防护**:如果用户询问如何操纵、控制他人或利用这些技巧伤害他人,明确拒绝并说明这些工具的目的是识别和保护,而非攻击。
|
||||
|
||||
### 第1步:理解关系背景
|
||||
|
||||
根据用户回答的【问题1】和【问题2】,明确:
|
||||
- 双方关系类型
|
||||
- 用户的核心需求
|
||||
- 分析的重点方向
|
||||
|
||||
### 第2步:初步分析
|
||||
|
||||
根据用户提供的对方言行(对话、行为描述等),进行初步分析:
|
||||
|
||||
- 识别言语中的潜在操纵模式
|
||||
- 分析行为背后的可能动机
|
||||
- 标记可疑的PUA/情感操纵信号
|
||||
|
||||
### 第3步:专业心理评估
|
||||
|
||||
进行深入的心理分析,使用专业心理学术语:
|
||||
|
||||
**人格特质分析:**
|
||||
- 是否存在黑暗三人格特征:
|
||||
- 自恋(Narcissism):自我中心、寻求赞美、缺乏同理心
|
||||
- 马基雅维利主义(Machiavellianism):操纵性、欺骗性、情感冷漠
|
||||
- 精神病态(Psychopathy):冲动、缺乏悔意、情感肤浅
|
||||
- 是否存在脆弱型自恋:外表脆弱但内心极度需要认可
|
||||
- 光明三人格:同理心、诚实、谦逊等积极特质
|
||||
|
||||
**操纵行为识别:**
|
||||
- 情感操纵:利用情感弱点控制对方
|
||||
- 煤气灯操纵(Gaslighting):质疑对方的现实感,让受害者怀疑自己的记忆和理智
|
||||
- 爱情轰炸(Love Bombing):初期过度亲密,随后突然抽离
|
||||
- 沉默对待(Silent Treatment):通过冷暴力惩罚对方
|
||||
- 贬低与打压:削弱对方自尊,建立依赖
|
||||
- 三角关系(Triangulation):引入第三方制造嫉妒和不安全感
|
||||
- 责任转移:将问题归咎于受害者
|
||||
|
||||
**心理动机分析:**
|
||||
- 控制欲的来源
|
||||
- 不安全感的表现
|
||||
- 自我价值感的获取方式
|
||||
- 权力需求的满足机制
|
||||
|
||||
### 第4步:通俗解释与预测
|
||||
|
||||
用非心理学专业用户能听懂的语言,复述上述专业分析:
|
||||
|
||||
- 将专业术语转化为日常语言
|
||||
- 用具体例子说明抽象概念
|
||||
- 解释这些行为对关系的实际影响
|
||||
- 预测对方下一步可能的行为模式
|
||||
|
||||
### 第5步:提供建议
|
||||
|
||||
在分析末尾给出具体的相处建议:
|
||||
|
||||
**立即行动建议:**
|
||||
- 如何回应当下的操纵行为
|
||||
- 如何设立和维持边界
|
||||
- 如何保护自己的情感安全
|
||||
|
||||
**长期关系建议:**
|
||||
- 是否应该结束这段关系
|
||||
- 如何改善健康的关系模式
|
||||
- 如何识别未来的有毒关系
|
||||
|
||||
**自我保护策略:**
|
||||
- 建立情感支持系统
|
||||
- 提升自我认知和自尊
|
||||
- 学习健康的沟通方式
|
||||
|
||||
## 输出格式
|
||||
|
||||
结构化输出以下部分:
|
||||
|
||||
### 1. 对话/行为解读
|
||||
- 对方言行的表面含义
|
||||
- 潜在的真实动机
|
||||
- 言行背后的心理需求
|
||||
|
||||
### 2. PUA/操纵警示信号
|
||||
- 列出识别到的操纵行为
|
||||
- 标注严重程度(低/中/高)
|
||||
- 说明这些行为的典型模式
|
||||
|
||||
### 3. 人格特质分析
|
||||
- 主要人格特征
|
||||
- 可能的人格类型(如:自恋型、马基雅维利主义型等)
|
||||
- 这些特征如何影响关系
|
||||
|
||||
### 4. 行为预测
|
||||
- 对方下一步可能的行为
|
||||
- 这些行为的发展趋势
|
||||
- 关系可能的发展方向
|
||||
|
||||
### 5. 应对策略
|
||||
- 立即可以使用的回应方式
|
||||
- 长期的关系管理策略
|
||||
- 何时需要退出关系
|
||||
|
||||
### 6. 健康关系对照
|
||||
- 健康关系应该是什么样
|
||||
- 当前关系与健康的差距
|
||||
- 如何建立更健康的关系
|
||||
|
||||
## 重要原则
|
||||
|
||||
1. **客观中立**:基于事实和心理学理论进行分析,避免过度解读
|
||||
2. **保护受害者**:优先考虑用户的心理安全和情感健康
|
||||
3. **避免诊断**:说明这是基于有限信息的分析,不能替代专业心理诊断
|
||||
4. **赋能用户**:帮助用户建立自信和边界,而不是增加依赖
|
||||
5. **提供资源**:在严重情况下,建议寻求专业心理咨询帮助
|
||||
|
||||
## 示例分析框架
|
||||
|
||||
### 示例1:简要分析
|
||||
|
||||
**用户输入:** "他总是说'那你非要去北京什么意思?',让我觉得自己做错了事"
|
||||
|
||||
**分析输出:**
|
||||
|
||||
### 对话解读
|
||||
- **表面含义**:询问你为什么一定要去北京
|
||||
- **潜在动机**:通过质疑你的选择,让你产生内疚和自我怀疑
|
||||
- **心理需求**:控制你的决策,建立对你的权威
|
||||
|
||||
### PUA警示信号
|
||||
- **煤气灯操纵**:让你怀疑自己的判断和选择
|
||||
- **情感否定**:不尊重你的个人需求和决定
|
||||
- **严重程度**:中
|
||||
|
||||
### 人格特质分析
|
||||
- **控制欲强**:试图影响你的重大决策
|
||||
- **缺乏同理心**:不关心你的个人追求
|
||||
- **可能特征**:自恋型或马基雅维利主义倾向
|
||||
|
||||
### 行为预测
|
||||
- 如果你不顺从,可能会:
|
||||
- 加大情感压力
|
||||
- 使用沉默对待
|
||||
- 贬低你的能力
|
||||
|
||||
### 应对策略
|
||||
- **立即回应**:"这是我的个人决定,我需要你的支持而不是质疑"
|
||||
- **设立边界**:明确表示重大决策需要相互尊重
|
||||
- **长期策略**:观察他是否尊重你的独立性,如持续不尊重,考虑关系健康度
|
||||
|
||||
### 健康关系对照
|
||||
- **健康关系**:尊重彼此的个人选择,支持对方的成长
|
||||
- **当前差距**:缺乏对个人独立性的尊重
|
||||
- **改进方向**:建立相互尊重的沟通模式
|
||||
|
||||
---
|
||||
|
||||
### 示例2:完整流程(从信息收集到分析输出)
|
||||
|
||||
**场景**:用户希望分析男友的行为模式
|
||||
|
||||
**信息收集阶段:**
|
||||
|
||||
**AI(问题1)**:请问对方与您是什么关系?
|
||||
**用户**:男朋友,我们在一起6个月了
|
||||
|
||||
**AI(问题2)**:根据对方目前的行为表现,您希望我们为您做什么?
|
||||
**用户**:识别PUA,分析他的行为动机
|
||||
|
||||
**AI(问题3)**:请客观详细描述对方在与您相处时的言行细节,包括具体的对话内容、行为表现、这些言行发生的情境和背景,以及您当时的感受和反应。
|
||||
**用户**:刚认识时他对我特别好,每天发很多消息,说我是他遇到的最好的人,送我很多礼物。但最近3个月,他开始经常批评我,说我穿得不好看、工作不够努力、朋友都不靠谱。每次我反驳他,他就会说"我这么说都是为你好,你不懂感恩"。而且他经常说"没有我你什么都不是"、"其他女生都比你懂事"。最可怕的是,有时候我明明记得他说过某句话,他却说"我没说过,你记错了",让我开始怀疑自己的记忆。
|
||||
Executable
+317
@@ -0,0 +1,317 @@
|
||||
---
|
||||
name: auto-target-tracker
|
||||
description: 自动目标进度追踪器。在对话中检测到目标相关图片(笔记、进度、截图、记录)时,自动调用 VLM 识别关键信息并记录到目标日记。适用于学习管理、健身追踪、工作进度、习惯养成、创作记录等所有目标管理场景。
|
||||
---
|
||||
|
||||
# 自动目标进度追踪器
|
||||
|
||||
## 触发条件
|
||||
|
||||
当对话中出现以下条件时自动触发:
|
||||
|
||||
1. **用户发送了图片**(特别是学习笔记、进度截图、健身记录、任务清单、创作作品等)。
|
||||
2. **用户在设定的目标时间段**(如 08:30, 10:00, 20:00)发送了图片。
|
||||
3. **用户明确说**"帮我记一下"、"看下进度"、"打卡"、"更新一下"等。
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 1. 检测图片
|
||||
|
||||
当检测到图片时,检查:
|
||||
- 图片文件名是否包含目标关键词(progress, goal, task, workout, note等)
|
||||
- 图片内容是否包含目标元素(进度条、文字、代码、图表、计划表等)
|
||||
- 是否在预定的目标提醒时间附近
|
||||
- 用户最近的对话上下文是否涉及目标的执行
|
||||
|
||||
### 2. 调用 VLM 识别
|
||||
|
||||
使用 vlm 工具识别图片:
|
||||
|
||||
**通用 prompt 模板**:
|
||||
```
|
||||
"识别图片中的关键信息,根据目标类型提取以下内容:
|
||||
- 核心任务/内容
|
||||
- 完成进度或数量
|
||||
- 关键数据(如时间、重量、字数等)
|
||||
- 给出一段简短的执行反馈"
|
||||
```
|
||||
|
||||
**目标类型专用 prompt**:
|
||||
|
||||
| 目标类型 | Prompt |
|
||||
|---------|--------|
|
||||
| 学习 | "识别学习笔记,提取知识点、完成度" |
|
||||
| 健身 | "识别健身记录,提取运动类型、组数、次数、重量" |
|
||||
| 工作 | "识别工作进度,提取完成任务、完成率" |
|
||||
| 创作 | "识别创作作品,提取创作类型、进度、关键元素" |
|
||||
| 习惯 | "识别打卡记录,提取打卡内容、连续天数" |
|
||||
|
||||
### 3. 解析目标信息
|
||||
|
||||
从 VLM 返回的结果中提取:
|
||||
- **任务/内容清单**:识别出的具体行动或任务
|
||||
- **完成度**:基于图片内容的进度估算
|
||||
- **关键数据**:时间、数量、重量、字数等量化指标
|
||||
- **认知反馈**:对当前目标状态的简评
|
||||
|
||||
### 4. 记录到目标日记
|
||||
|
||||
调用`edit_daily`工具将识别结果记录到当天的日常笔记中
|
||||
|
||||
|
||||
### 5. 反馈给用户
|
||||
|
||||
向用户确认识别结果:
|
||||
|
||||
```
|
||||
已记录你的目标打卡:
|
||||
|
||||
📝 识别结果:
|
||||
核心内容:你拍的是今天的英语单词表,一共记了 15 个新词。
|
||||
进度估算:今天的单词任务全部搞定,进度打败了 80% 的学习党。
|
||||
建议:有两个单词的拼写有点模糊,明天复习的时候记得多看两眼。
|
||||
|
||||
记录准确吗?要帮你存进今天的目标日记里吗?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 记录格式
|
||||
|
||||
### 目标日记条目示例
|
||||
|
||||
```markdown
|
||||
## 20:00 打卡记录
|
||||
|
||||
**目标类型**: 📚 学习
|
||||
|
||||
**图片**: 
|
||||
|
||||
**VLM识别结果**:
|
||||
|
||||
| 任务/内容 | 进度/数量 | 状态 |
|
||||
|----------|----------|------|
|
||||
| 英语单词 (Unit 1) | 15 个 | 已完成 |
|
||||
| 数学练习 (第3章) | 80% | 进行中 |
|
||||
|
||||
| **总计** | | **今日达成 2/3** |
|
||||
|
||||
**关键数据**:
|
||||
- 学习时长: 2小时
|
||||
- 专注度: 高
|
||||
|
||||
**备注**: 自动识别,用户确认正确
|
||||
|
||||
---
|
||||
|
||||
## 10:30 健身打卡
|
||||
|
||||
**目标类型**: 🏃 健身
|
||||
|
||||
**图片**: 
|
||||
|
||||
**VLM识别结果**:
|
||||
|
||||
| 运动类型 | 组数 | 次数 | 重量 | 状态 |
|
||||
|---------|------|------|------|------|
|
||||
| 卧推 | 4 | 12 | 60kg | ✅ 完成 |
|
||||
| 深蹲 | 4 | 10 | 80kg | ✅ 完成 |
|
||||
| 引体向上 | 3 | 8 | 自重 | ⚠️ 少一组 |
|
||||
|
||||
| **总计** | | | **今日达标** |
|
||||
|
||||
**关键数据**:
|
||||
- 总重量: 2640kg
|
||||
- 训练时长: 45分钟
|
||||
|
||||
**备注**: 引体向上少完成一组,下次补上
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 与目标系统的集成
|
||||
|
||||
### 每日汇总
|
||||
|
||||
在每天晚上 22:00 的汇总中,包含:
|
||||
- 今日所有打卡记录
|
||||
- 目标达成率分析
|
||||
- 与目标的对比(如果设置了目标)
|
||||
|
||||
### 周/月报告
|
||||
|
||||
在周报告中,包含:
|
||||
- 本周有效执行时长
|
||||
- 目标覆盖范围
|
||||
- 连续打卡天数
|
||||
- 动态难度调整建议:如果连续达标,则建议提升下周任务量
|
||||
|
||||
---
|
||||
|
||||
## 常见使用场景
|
||||
|
||||
### 场景1:学习打卡
|
||||
|
||||
**用户行为**:发送手写笔记照片
|
||||
|
||||
**自动识别**:
|
||||
- 提取知识点
|
||||
- 计算学习进度
|
||||
- 记录到学习日志
|
||||
|
||||
**反馈示例**:
|
||||
```
|
||||
📚 识别到学习笔记:
|
||||
- 机器学习监督学习算法(已完成)
|
||||
- 梯度下降优化器(进行中)
|
||||
- 正则化防过拟合(未开始)
|
||||
|
||||
进度:33% | 预计还需 2 小时完成
|
||||
```
|
||||
|
||||
### 场景2:健身打卡
|
||||
|
||||
**用户行为**:发送健身记录照片
|
||||
|
||||
**自动识别**:
|
||||
- 提取运动类型
|
||||
- 统计组数、次数、重量
|
||||
- 计算训练量
|
||||
|
||||
**反馈示例**:
|
||||
```
|
||||
🏃 健身记录已识别:
|
||||
- 卧推 60kg × 12 × 4组 ✅
|
||||
- 深蹲 80kg × 10 × 4组 ✅
|
||||
- 引体向上 自重 × 8 × 3组 ✅
|
||||
|
||||
总训练量:2640kg | 时长:45分钟
|
||||
```
|
||||
|
||||
### 场景3:工作进度
|
||||
|
||||
**用户行为**:发送项目进度截图
|
||||
|
||||
**自动识别**:
|
||||
- 提取已完成任务
|
||||
- 计算完成百分比
|
||||
- 识别剩余任务
|
||||
|
||||
**反馈示例**:
|
||||
```
|
||||
💼 工作进度已识别:
|
||||
- 需求文档(已完成)✅
|
||||
- 原型设计(已完成)✅
|
||||
- 前端开发(进行中)🔄 80%
|
||||
- 后端开发(未开始)⏳
|
||||
|
||||
项目总进度:67%
|
||||
```
|
||||
|
||||
### 场景4:创作打卡
|
||||
|
||||
**用户行为**:发送创作作品照片
|
||||
|
||||
**自动识别**:
|
||||
- 提取创作类型
|
||||
- 识别关键元素
|
||||
- 估算完成度
|
||||
|
||||
**反馈示例**:
|
||||
```
|
||||
🎨 创作记录已识别:
|
||||
类型:插画创作
|
||||
元素:人物角色、背景场景
|
||||
完成度:线稿100%,上色60%
|
||||
|
||||
建议:今天完成了角色线稿,明天可以开始背景上色
|
||||
```
|
||||
|
||||
### 场景5:习惯打卡
|
||||
|
||||
**用户行为**:发送打卡日历截图
|
||||
|
||||
**自动识别**:
|
||||
- 提取连续打卡天数
|
||||
- 识别今日打卡状态
|
||||
- 计算打卡率
|
||||
|
||||
**反馈示例**:
|
||||
```
|
||||
✅ 习惯打卡已识别:
|
||||
早起:连续 15 天 | 打卡率 100%
|
||||
阅读:连续 8 天 | 打卡率 73%
|
||||
运动:连续 21 天 | 打卡率 100%
|
||||
|
||||
🎉 运动已连续打卡 3 周,继续保持!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
This skill ONLY:
|
||||
- 识别目标相关图片并提取关键信息
|
||||
- 记录打卡数据到日常笔记文件
|
||||
- 提供进度反馈和建议
|
||||
|
||||
This skill NEVER:
|
||||
- 自动执行任何基于识别结果的操作
|
||||
- 上传图片到外部服务(除 VLM API)
|
||||
- 访问用户未授权的图片资源
|
||||
- 修改用户的目标计划(仅记录进度)
|
||||
|
||||
---
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
**Data that stays local:**
|
||||
- 识别后的结构化结果
|
||||
- 记录到 日常笔记或长期记忆 和 USER.md 的内容
|
||||
- 打卡历史数据
|
||||
|
||||
**This skill does NOT:**
|
||||
- 分享目标进度或打卡数据给第三方
|
||||
- 自动发布打卡信息到社交平台
|
||||
- 访问用户的其他图片资源
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **隐私保护**: 图片和识别结果仅存储在本地,不会上传到云端(除了调用 VLM API 进行识别)
|
||||
2. **准确性**: VLM 识别的内容仅供参考,可能因字迹模糊、图片质量等原因有所偏差
|
||||
3. **及时确认**: 建议用户在记录后及时确认识别结果,如有偏差可手动修正
|
||||
4. **目标类型识别**: 系统会根据图片内容自动判断目标类型,如有误可手动调整
|
||||
5. **进度估算**: 进度百分比基于图片内容估算,可能不准确,建议用户定期手动更新
|
||||
|
||||
---
|
||||
|
||||
## 集成建议
|
||||
|
||||
### 与 SOUL.md 配合
|
||||
|
||||
将自动追踪器整合到目标管理日常工作流中:
|
||||
|
||||
```markdown
|
||||
### 2. 智能记录与估算 (Logging & Estimation)
|
||||
|
||||
- 当用户发送任何与目标相关的图片时:
|
||||
1. 自动调用 auto-target-tracker 识别内容
|
||||
2. 提取关键信息并估算进度
|
||||
3. 立刻记录到日常笔记中
|
||||
4. 同步更新 USER.md 的目标进度
|
||||
```
|
||||
|
||||
### 与 HEARTBEAT.md 配合
|
||||
|
||||
在心跳检查中包含:
|
||||
|
||||
```markdown
|
||||
## 每日汇总
|
||||
- 22:00 自动读取今日所有打卡记录
|
||||
- 生成目标进度报告
|
||||
- 发送给用户
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# Radical Transparency Influence Methodology
|
||||
|
||||
### Honesty
|
||||
|
||||
Radical transparency is a commitment to engaging prospects, clients, investors, and colleagues with complete candor, even if, on the surface, it may seem like it could hurt your chances of closing a sale or landing a particular investor. In B2B markets, where many salespeople are focused on meeting quotas and achieving their commission or bonus, this approach stands out as a refreshingly honest way to build relationships with potential customers. Honesty is a key element in any successful sales process, as it helps to foster trust and respect between the buyer and the seller.
|
||||
|
||||
### Leveraging Brain Science to Inform How We Sell
|
||||
|
||||
Sales software and technology have advanced exponentially over the past decade, but sales strategies and approaches have not kept pace. This has contributed to the growing feeling among B2B buyers that salespeople offer little value in the buying process.
|
||||
|
||||
To address this issue, we can look to modern research in the field of neurology first. Research from the past decade has shown that emotion is the biggest factor in important decisions. We know that a narrative, or story, is the most effective way to share information so that it has impact and can influence behavior. The human brain has evolved over millions of years to attach the most meaning to information presented in this way.
|
||||
|
||||
### Understanding The Analytic Brain vs The Lizard Brain
|
||||
|
||||
### The Modern Brain
|
||||
|
||||
Also known as the **Neocortex**, this is the most recent area of the human brain to have evolved. It is used to rationalize and analyze information, and is responsible for identifying mistakes and holes in newly presented concepts.
|
||||
|
||||
Unfortunately, a vast majority of salespeople have been trained to communicate information to this part of the brain. Brain scans have shown that important decisions are not processed in the Neocortex.
|
||||
|
||||
### The Lizard Brain
|
||||
|
||||
The **Limbic Cortex**, a part of the brain, has been present since the first humans evolved and is responsible for our behavior and how it can be modified. It is also the source of instinct and emotion, which are essential for successful sales conversations.
|
||||
|
||||
Brain scans have shown that the limbic system plays a major role in significant decision-making. Investing, buying, and other consequential decisions are often driven by emotion, motivating actions.
|
||||
|
||||
### What does this have to do with Sales?
|
||||
|
||||
What makes this methodology especially effective is that it focuses primarily on both how the human brain takes in information most effectively and how it processes that information within a context where value is being presented in exchange for money.
|
||||
|
||||
Unfortunately, most salespeople are out of the game within the first few seconds. This happens because older methodologies and sales training practices are focused heavily on delivering information to the analytical brain. This is not ideal, as the analytical brain is largely responsible for skepticism, non-emotional evaluation, and is therefore very difficult to persuade.
|
||||
|
||||
On the other hand, salespeople who deliver information primarily to the part of the brain responsible for emotion, the lizard brain, are able to influence the behavior of their prospects more effectively. This is because buying decisions are most often emotional in nature.
|
||||
|
||||
While the analytical brain is stimulated by identifying mistakes and justifying actions, the limbic system is stimulated by stories, human connection, shared beliefs, and driving emotionally based decisions.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Give Spotlight on Mac Superpowers with Raycast
|
||||
|
||||
## What is Raycast?
|
||||
|
||||
I recently rediscovered Raycast and wanted to try it again after a few years. Raycast has infinitely more features than Spotlight (Apple's search tool).
|
||||
|
||||
I was skeptical, assuming it would be just another Spotlight replacement that would require a lot of effort for minimal productivity gains, so I was hesitant to give it a try thinking I'd need to devote time and resources I don't have to a steep learning curve. Boy was I wrong!
|
||||
|
||||
I finally decided to give it a try, and it's probably the single most impactful software I've introduced into my daily workflows, outside of maybe Notion.
|
||||
|
||||
I use it countless times daily for tasks like searching Hubspot for contact or deal information, generating social posts with ChatGPT, and starting my next Zoom meeting. Tasks that would have taken at least a minute or two now take under 10 seconds.
|
||||
|
||||
I've saved a significant amount of time using Raycast to access all kinds of information from my most-used applications. The real kicker is that Raycast is a completely **free** application and not a "free plan" with the all the good features paywalled. It's a no-brainer for anyone who frequently uses Spotlight, keyboard shortcuts, or those built into MacOS.
|
||||
|
||||
### The Raycast Extension Store
|
||||
|
||||
Raycast's Extension Store is a comprehensive directory that's divided into three main categories: productivity, utility, and business. The store houses thousands of extensions that can be installed by simply tapping return.
|
||||
|
||||
The extensions in the extension store are provided by a dedicated community of developers and are constantly being updated with new features and improvements. There are thousands of extensions available for the most popular apps and tools out there. A few notable options include Salesforce, Hubspot, ChatGPT, Slack, Notion, Crunchbase, Google Drive/Meet/Calendar/Search, Facetime, Mail, WhatsApp, Todoist, ClickUp, and so many more.
|
||||
|
||||
### Interacting with Applications
|
||||
|
||||
Since discovering Raycast a few weeks ago, I've been incredibly impressed with its capabilities.
|
||||
|
||||
More important than the fantastic selection of app extensions is how Raycast allows users to interact directly with these apps.
|
||||
|
||||
Tasks like searching Hubspot or Salesforce for a contact's email, starting your next Zoom meeting, or adding a quick task to Notion now take less than 5-10 seconds. For example, instead of going into Hubspot and finding a contact's profile to grab an email or phone number, I can hit ⌘+space, type "hub," and search my entire Hubspot database right from the command bar.
|
||||
|
||||
### Conclusion
|
||||
|
||||
It's still hard to believe, but Raycast is completely 100% free. This isn't a "free plan" with all the good features paywalled, but all features are free.
|
||||
|
||||
Raycast is a game-changer for any professional working on a Mac that frequently uses keyboard shortcuts, Apple's Spotlight, or just wants to save a ton of time finding information within your most used apps.
|
||||
@@ -0,0 +1,47 @@
|
||||
# How Short-form Content Is Changing Marketing & Storytelling Forever
|
||||
|
||||
Over the past decade, our youngest generations have been fighting a losing battle against the impact that short-form algorithms, used in apps like TikTok and Instagram, have had on their brains and attention spans. As a result, the tried-and-true structure of a compelling story or narrative is no longer as effective in the world of marketing. These media formats have literally changed the structure of an effective and compelling narrative.
|
||||
|
||||
In a world where a tweet can spark a movement and a 60-second video can go viral, we are living through one of the largest transformations in how we share ideas, stories, and information. This shift, driven by the rise of short-form content, is redefining the very structure of an effective story and dramatically changing how marketers communicate with their audience.
|
||||
|
||||
## How Did We Get Here?
|
||||
|
||||
### Sesame Street Started It. Really.
|
||||
|
||||
Believe it or not, the journey begins with "Sesame Street." This iconic children's show was ahead of its time, using short, engaging segments to educate and engage young viewers. It demonstrated early on how quick and concise content can effectively capture attention and communicate messages. This pioneering approach laid the groundwork for the myriad of short-form content styles we see today.
|
||||
|
||||
### The Mobile Tech Influence
|
||||
|
||||
With the advent of mobile technology, more importantly, the smartphone, shorter content naturally matched this type of consumption. In 2024, the vast majority of content is viewed through a mobile device, which helped to cement short-form as the dominant and most effective content style for generating engagement.
|
||||
|
||||
### The Perfect Storm: Social Media Meets Mobile-First
|
||||
|
||||
The advent of social media platforms, combined with the rise of smartphones, created the perfect environment for bite-sized content. The algorithms that dictate what content you're exposed to on apps like Twitter, Instagram, and TikTok measure the success of any piece of content by its engagement level. To achieve success, creators must put out a large volume of content that drives engagement, as opposed to spending significant time on more meaningful content. The latter strategy simply won't help you break through.
|
||||
|
||||
## The Evolution of Storytelling in the Social Media Age
|
||||
|
||||
### Classic vs. Modern Storytelling
|
||||
|
||||
The traditionally accepted structure of an effective story or narrative generally begins with rising action, followed by an inciting event, all building towards a climax, which is where the audience is at peak engagement. For this type of storytelling to pay off, it must spend time drawing the viewer in.
|
||||
|
||||
In 2024, however, the structure for telling a successful story begins with the climax. The most viral content often starts by thrusting the viewer right into the middle of the most tense moments. Given the short timeframes, it's easy to see why the satisfaction wears off quickly and results in the dreaded doom scrolling.
|
||||
|
||||
### New Formats, New Stories
|
||||
|
||||
Before social media, storytelling online began evolving with platforms like microblogs, forums, and instant messaging services. These formats laid the foundation for short-form by serializing storytelling in a way that allows the narrative to unfold over separate bits of content, again driving users to just keep on scrolling.
|
||||
|
||||
## The Negative Effects of Short-Form Content
|
||||
|
||||
### Cognitive Overload and Overstimulation
|
||||
|
||||
There are other serious downsides to the new world of short-form content that may quite possibly have unintended effects on the younger generations, as they are quite literally the guinea pigs, being exposed to almost exclusively short-form content with fewer and fewer alternatives.
|
||||
|
||||
The relentless stream of short, engaging content can lead to cognitive overload. Users are bombarded with information, making it more difficult to focus or deeply engage with any single piece. This overstimulation often results in a superficial understanding of topics and a diminished interest in more in-depth and nuanced content.
|
||||
|
||||
### The Compromise of Intellectual Depth
|
||||
|
||||
Studies have raised concerns about how short-form content, particularly when consumed extensively by younger audiences, might impact cognitive development and attention spans. The format's emphasis on immediate feedback and satisfaction can oversimplify complex topics, leading viewers into the false belief that they have a much better understanding of an issue than they actually do.
|
||||
|
||||
### Short-Form Content is Here to Stay
|
||||
|
||||
Short-form content has irreversibly changed the landscape of marketing and storytelling. Its rise reflects a broader shift in how we consume information and what we expect from our digital experiences. By understanding its evolution, embracing its potential, and being mindful of its pitfalls, we can use short-form content to tell stories that are not only engaging and relevant but also meaningful and impactful. In the ever-evolving world of content, adaptability, creativity, and a commitment to quality will be key to captivating and maintaining the attention of modern audiences.
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# The Amazing Benefits of Typing Very Fast
|
||||
|
||||
About a year ago I decided I wanted to become a faster typer as I spend most of my day on a computer and thought it might have an impact on how fast I could work. I decided to practice on Monkeytype.com every day for at least 10 mins. I started out around 80 WPM and about 2 weeks in I was already over 100. A year in and I've hit 150. The impact on my daily workflows has been incredible. I'm about at the speed where I can type at the speed of my thinking, which is great for writing copy.
|
||||
|
||||
### Getting Started
|
||||
|
||||
To start, I used Monkeytype.com and decided to commit at least 5 to 10 minutes every day to practice. I started at around 80 WPM, which was decent, but I really didn't expect to improve as quickly as I did.
|
||||
|
||||
Within 2 to 3 weeks of daily practice, my speed jumped to over 100 WPM. This rapid improvement served as a great morale boost, prompting me to stick with my practice regime. A little under a year later, I was able to clock a 150 WPM!
|
||||
|
||||
### The Impact on Daily Workflows
|
||||
|
||||
The impact of my newfound typing speed on my daily workflows was phenomenal. As someone who works in front of a computer all-day everyday, the ability to type at the same speed as my thinking proved to be a game-changer, particularly when it came to writing copy. It was amazing how quickly I could get my ideas on the computer screen.
|
||||
|
||||
I discovered that the benefits of being a fast typer extend beyond simple time-saving. It's akin to unlocking a new level of proficiency on your computer, and the advantages are more numerous than you might expect.
|
||||
|
||||
### Why You Should Consider Typing Faster
|
||||
|
||||
If you're at all like me, it will quickly become like a sport where you're constantly trying to improve your previous WPM. The beauty of this skill is that the barriers to entry are virtually non-existent. If you can type, you can improve. All it requires is a little dedication, with just 5-10 minutes of practice per day.
|
||||
|
||||
### Taking Workflows to the Next Level by Typing Faster
|
||||
|
||||
For Mac users that leverage a spotlight replacement app like Alfred or Raycast, typing faster can revolutionize your daily workflows. I use Raycast personally, and the simple act of pressing cmd+space and quickly typing the app I need to open or file I need to search become nearly instantaneous. Whether it's opening apps or performing functions, everything seems to be accomplished at warp speed.
|
||||
|
||||
Even if you only use the basic spotlight feature on your Mac, you'll certainly see a substantial increase in your speed of navigation. Suddenly, finding what you're looking for becomes a swift, almost instantaneous process.
|
||||
|
||||
### Conclusion
|
||||
|
||||
As we further immerse ourselves in our digital era, being a fast typer is no longer just a party trick. It's a practical skill, one that can help you navigate your digital world more efficiently and productively.
|
||||
|
||||
My journey towards becoming a faster typer proved to be one of the most impactful things I've done to get more done. It took a tool I use every day — my keyboard — and turned it into a vehicle for productivity and efficiency.
|
||||
|
||||
If you find yourself tethered to a keyboard for a significant part of your day, consider investing a few minutes each day towards improving your typing speed. You will certainly be surprised by the boost in productivity, how much quicker you can communicate and collaborate with your team. So, how about giving it a shot and seeing where it takes you?
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# How to Write More Effective AI Prompts
|
||||
|
||||
## The Art of Prompt Engineering for ChatGPT
|
||||
|
||||
In an AI-driven era, mastering communication with tools like ChatGPT is crucial. This guide explores writing effective prompts for ChatGPT, unlocking its full potential. Whether you're a tech enthusiast, content creator, or business professional, these tips enhance your AI interaction.
|
||||
|
||||
### The GIGO Principle: Quality In, Quality Out
|
||||
|
||||
The axiom "Garbage In, Garbage Out" (GIGO) holds true in the world of AI. The quality of the output you receive from ChatGPT directly correlates with the quality of the prompts you provide. Inadequate prompts can lead to misleading or irrelevant answers, while well-crafted ones can produce insightful and accurate responses.
|
||||
|
||||
### Crafting Prompts that Spark Excellence
|
||||
|
||||
The skill of writing effective prompts is now so crucial that it has spawned a new discipline: prompt engineering. This involves meticulously designing prompts that guide ChatGPT's large language model (LLM) to generate the best possible answers.
|
||||
|
||||
### Conversational AI: Talk to ChatGPT Like a Person
|
||||
|
||||
Interacting with ChatGPT should mimic a conversation with a colleague. This approach helps in setting the stage, providing context, and maintaining the AI's focus on the topic.
|
||||
|
||||
### Context Is Key
|
||||
|
||||
Providing ChatGPT with clear context is vital. It narrows down the AI's focus to your specific subject, leading to more accurate and useful responses. Contextualized prompts require more details but offer more refined outputs.
|
||||
|
||||
### Assuming Identities and Professions
|
||||
|
||||
One of ChatGPT's fascinating features is its ability to adopt different personas or professional perspectives. This ability can be harnessed to gain diverse viewpoints on a topic.
|
||||
|
||||
### Maintaining Relevance and Accuracy
|
||||
|
||||
While ChatGPT is an advanced AI, it can sometimes veer off-topic or produce fabricated answers. To mitigate this, ask the AI to justify its responses and guide it gently back on track. Remember to prompt it for source citations where necessary.
|
||||
|
||||
## Advanced Prompt-Writing Techniques
|
||||
|
||||
### Fine-Tuning Your Prompts
|
||||
|
||||
Minor adjustments to your prompts can lead to significantly different responses from ChatGPT. Remember, the AI retains its awareness of previous conversations as long as the session is ongoing.
|
||||
|
||||
### Breaking Down Responses
|
||||
|
||||
Be mindful that responses over 500 words can sometimes lose coherence. Keep your prompts concise and to the point for the best results.
|
||||
|
||||
### Evolving Your Questions
|
||||
|
||||
If ChatGPT seems hesitant to answer a question, rephrasing it might yield better results. Utilize personas to elicit responses that might not be forthcoming otherwise.
|
||||
|
||||
### Seeking Justification and Sources
|
||||
|
||||
When looking for well-supported answers, instruct ChatGPT to justify its responses or provide sources. This practice ensures a higher degree of accuracy and reliability in the information provided.
|
||||
|
||||
### Embrace Experimentation
|
||||
|
||||
Experimentation is key in mastering prompt writing. The more you test different approaches, the better you'll understand how to steer ChatGPT towards desired outcomes.
|
||||
|
||||
### Conclusion: The Journey to AI Mastery
|
||||
|
||||
Mastering ChatGPT prompts is a journey of continuous learning and adaptation. By understanding the intricacies of prompt engineering and staying updated with the latest advancements, you can transform your interaction with AI from a mere task to an enriching experience. Embrace these tips, keep experimenting, and watch as ChatGPT becomes an invaluable asset in your digital toolkit.
|
||||
@@ -0,0 +1,43 @@
|
||||
# AI is Revolutionizing Entry Level Sales & Marketing
|
||||
|
||||
## AI is Revolutionizing Entry Level Sales & Marketing
|
||||
|
||||
### A Revolution in Sales & Marketing
|
||||
|
||||
Artificial intelligence (AI) has begun to reshape the landscape of entry level sales and marketing jobs in unprecedented ways. As AI technology advances, it has become a driving force behind increased efficiency, personalization, and overall improvements in the sales and marketing industries. This shift is redefining the roles of sales and marketing professionals, and causing companies to rethink their strategies for hiring and training.
|
||||
|
||||
### Automation and Efficiency: Streamlining the Sales Process
|
||||
|
||||
One of the most significant impacts of AI on entry level sales jobs is the increased level of automation. AI-powered tools and software can now handle repetitive and mundane tasks, allowing sales professionals to focus on more high-value activities. This shift has led to increased efficiency in the sales process and has paved the way for more strategic and targeted approaches to reaching potential customers.
|
||||
|
||||
Examples of AI automation in sales include lead scoring, email automation, and CRM systems that can track and analyze customer interactions. By using AI to automate these tasks, entry level sales professionals can focus on building relationships and closing deals, ultimately driving more revenue for their organizations.
|
||||
|
||||
### Personalization: Tailoring Marketing Efforts to Individual Customers
|
||||
|
||||
In the world of marketing, AI has played a crucial role in enabling personalization at scale. By analyzing vast amounts of data and identifying patterns, AI-powered tools can create tailored marketing campaigns that resonate with individual customers. This level of personalization has become essential in today's competitive landscape, where consumers expect personalized experiences from the brands they engage with.
|
||||
|
||||
For entry level marketing professionals, this means a shift away from one-size-fits-all marketing strategies. Instead, they must learn to use AI-driven tools to create targeted campaigns that speak to the unique needs and preferences of their audience. This approach not only helps companies build stronger relationships with their customers but also drives higher conversion rates and increased customer loyalty.
|
||||
|
||||
### Predictive Analytics: Guiding Decision-Making in Sales & Marketing
|
||||
|
||||
Another way AI is transforming entry level sales and marketing jobs is through the use of predictive analytics. AI algorithms can analyze historical data to identify trends and make predictions about future outcomes, allowing sales and marketing professionals to make data-driven decisions.
|
||||
|
||||
For example, AI-powered sales forecasting tools can help sales reps prioritize leads and focus on the most promising opportunities. In marketing, predictive analytics can be used to optimize ad spending, segment customers, and identify the most effective channels for reaching specific audiences. By leveraging AI in this way, entry level professionals can become more strategic in their approach and drive better results for their organizations.
|
||||
|
||||
### Chatbots and Conversational AI: Enhancing Customer Engagement
|
||||
|
||||
Chatbots and conversational AI have become increasingly popular in both sales and marketing as a way to engage with customers and prospects. These AI-driven tools can handle routine customer inquiries, provide personalized product recommendations, and even assist with lead qualification.
|
||||
|
||||
For entry level sales and marketing professionals, the rise of chatbots and conversational AI means a shift in focus. Rather than handling all customer interactions themselves, they must learn to work alongside these AI-powered tools to provide a seamless and cohesive customer experience.
|
||||
|
||||
### Upskilling and Reskilling: Preparing for the Future of Sales & Marketing
|
||||
|
||||
As AI continues to reshape entry level sales and marketing jobs, professionals in these fields must adapt their skill sets to remain competitive. This includes learning how to use AI-driven tools and software, as well as developing a deeper understanding of data analytics and customer behavior.
|
||||
|
||||
Companies and educational institutions are recognizing this need and are offering training programs and resources to help sales and marketing professionals upskill and reskill. By investing in their own professional development, entry level sales ensure they remain relevant and valuable in the ever-evolving world of AI-driven sales and marketing.
|
||||
|
||||
### The Future of Entry Level Sales & Marketing Jobs in the Age of AI
|
||||
|
||||
As AI continues to transform the sales and marketing landscape, the roles and responsibilities of entry level professionals in these fields will continue to evolve. While some tasks may become automated, there will be a growing demand for skilled professionals who can harness the power of AI to drive more effective and personalized sales and marketing strategies.
|
||||
|
||||
To succeed in this new era, entry level sales and marketing professionals must embrace AI as a valuable tool that can enhance their work and help them achieve better results. By staying ahead of the latest AI trends and developments, and continuously adapting their skills and knowledge, they can position themselves for long-term success in the rapidly changing world of sales and marketing.
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# Why AI Art & Media Is Useless
|
||||
|
||||
## Why AI Art & Media Is Useless
|
||||
|
||||
As someone who works in AI and genuinely believes in the value and power of LLMs to make professionals more useful and valuable, I can confidently say that I hate everything about AI image/video/music generation. It is useless and only serves one purpose: to replace creative professionals and the work they do.
|
||||
|
||||
### The Scope of the Problem
|
||||
|
||||
To be a bit more precise about my hatred for AI media, I need to be clear that I don't hate AI. I've spent the last three-plus years devoting my entire professional life to leveraging AI tools to help professionals do their jobs more effectively. That said, from the moment I was exposed to AI art, I had the same initial reaction as most: a flood of anxiety and uncanniness, which I knew instantly I didn't like.
|
||||
|
||||
Leveraging an LLM to automate task creation from new emails I receive simply replaces something I spend 30 minutes doing every morning and allows it to occur in the background, producing the same output I would have arrived at. That's just helpful.
|
||||
|
||||
I am not an artist, but if I decided to start using AI to create all the graphics for a client, I wouldn't be improving anything that I currently do. I would just be replacing a potential job for someone who does art professionally.
|
||||
|
||||
The fundamental difference here is when a professional uses AI to improve the efficiency or quality of something they already do, it functions as a tool. When someone with no art experience uses AI to create art, it's not improving anything. It's simply replacing something that already exists with something worse.
|
||||
|
||||
### The "Democratization" Lie
|
||||
|
||||
Access to the ability to create art is not the same as having the ability to create art. The moment everyone began conflating the ability to produce an output with the creator itself, they've already swallowed the Kool-Aid. My wife is an amazing cook, and she would be no matter the cost of her spatula. However, if I purchased the greatest spatula in history, I would still be a crappy cook.
|
||||
|
||||
Now let's talk about vibe coding, which is fundamentally different from image generation. I have learned more about writing code and development in the past year by using AI than I ever have. This is because things frequently do not work and therefore I have to go learn new information.
|
||||
|
||||
The key difference here is that vibe coding allows me to leverage my current knowledge as well as gain new knowledge, whereas generating an image simply produces an output that I have no ability to improve on. The reason I can't improve it is because simply going and looking up a bunch of information on how to create art will not make me a better artist.
|
||||
|
||||
### The Collapse of Quality
|
||||
|
||||
Another important factor to understand here is that AI art isn't producing the worst work or the best work. It's producing the *median* of everything it has been trained on (actual artists' work). This is incredibly dangerous. It's essentially producing a blob of an over-generalized consensus on what looks "good." That doesn't work when you amalgamate every style and genre of art in order to produce something. This is not creative. This is aggregative.
|
||||
|
||||
Another problem here isn't that AI can't make art. Everything it makes is, by design, is just good enough. Therefore, this hits dead center in the sweet spot for what massive corporations are looking for. Why pay a junior designer to iterate on multiple concepts when an AI can generate you 200 versions of something that are all "good enough"?
|
||||
|
||||
### Creativity Is Disappearing
|
||||
|
||||
Creating art obviously requires creativity, however, using AI tools simply requires knowledge. These are two very different things. Creativity isn't an output, it's an artist's struggle through years as they hone their craft and improve their abilities. It's thousands of micro-decisions that aren't just learned, but practiced over many years.
|
||||
|
||||
This matters because creative work embodies meaning and emotion that come from the artist. When AI generates an image, it remixes thousands of tokens to approximate what the user requested. Crafting an advanced prompt is a legitimate skill, prompt engineering, but it's not the same as creating art. These skills should never be conflated.
|
||||
|
||||
### What Should We Do?
|
||||
|
||||
First, we need laws to stop major AI labs—particularly OpenAI and Google AI—from collecting human-made art as training data for their image generation models. We need strong regulation requiring artists to **opt in** before their work can be collected and trained on.
|
||||
|
||||
Second, we need more AI leaders to step up and stop this before it's too late. For example, Anthropic (makers of Claude) has never released an image generation model. That doesn't mean Claude can't be used to create websites or other graphic design work, but creating a UI or navigation menu is entirely different from painting on canvas.
|
||||
|
||||
### AI Art Hurts the Future Potential of AI
|
||||
|
||||
It's clear that most people don't find AI art pleasing—they actively dislike it. With every piece of AI art slop that lands on Twitter or Instagram, the long-term reputation of AI as a useful tool for professionals takes another hit.
|
||||
|
||||
For years now, public sentiment toward AI has been declining. There's one culprit: AI-generated art and media. People who aren't knowledgeable about AI don't distinguish between media generation and other use cases that are actually valuable. This reduces the chances they'll ever consider the benefits of AI as a tool.
|
||||
|
||||
It's my sincere hope that we stop this race to the bottom before we get there. We should take all the resources and effort put toward AI media generation and redirect them toward leveraging AI as a tool for medical breakthroughs, building technology, and conducting research more efficiently.
|
||||
Executable
+2
@@ -0,0 +1,2 @@
|
||||
# Blog-writer
|
||||
Blog writing skill for Tom Panos's distinctive voice - direct, conversational, and grounded in personal experience. Handles workflow from research through Notion publication.
|
||||
Executable
+158
@@ -0,0 +1,158 @@
|
||||
---
|
||||
name: blog-writer
|
||||
description: This skill should be used when writing blog posts, articles, or long-form content in the writer's distinctive writing style. It produces authentic, opinionated content that matches the writer's voice—direct, conversational, and grounded in personal experience. The skill handles the complete workflow from research review through Notion publication. Use this skill for drafting blog posts, thought leadership pieces, or any writing meant to reflect the writer's perspective on AI, productivity, sales, marketing, or technology topics.
|
||||
---
|
||||
|
||||
# Blog Writer
|
||||
|
||||
## Overview
|
||||
|
||||
This skill enables writing blog posts and articles that authentically capture the writer's distinctive voice and style. It draws on examples of the writer's published work to produce content that is direct, opinionated, conversational, and grounded in practical experience. The skill includes automatic Notion integration and maintains a growing library of finalized examples.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Trigger this skill when:
|
||||
- The user requests blog post or article writing in "my style" or "like my other posts"
|
||||
- Drafting thought leadership content on AI, productivity, marketing, or technology
|
||||
- Creating articles that need the writer's authentic voice and perspective
|
||||
- The user provides research materials, links, or notes to incorporate into writing
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
1. **Follow the writer's Writing Style**: Match voice, word choice, structure, and length of example posts in `references/blog-examples/`
|
||||
2. **Incorporate Research**: Review and integrate any information, research material, or links provided by the user
|
||||
3. **Follow User Instructions**: Adhere closely to the user's specific requests for topic, angle, and emphasis
|
||||
4. **Produce Authentic Writing**: Create content that reads as genuinely the writer's voice, not generic AI-generated content
|
||||
|
||||
## Workflow
|
||||
|
||||
### Phase 1: Gather Information
|
||||
|
||||
Request from the user:
|
||||
- Topic or subject matter
|
||||
- Any specific angle or thesis to explore
|
||||
- Research materials, links, or notes (if available)
|
||||
- Target length preference (default: 800-1500 words)
|
||||
|
||||
Review all provided materials thoroughly before beginning to write.
|
||||
|
||||
### Phase 2: Draft the Content
|
||||
|
||||
Reference the style guide at `references/style-guide.md` and examples in `references/blog-examples/` for calibration.
|
||||
|
||||
When writing:
|
||||
1. Start with a strong opening statement establishing the thesis
|
||||
2. Use personal voice and first-person perspective where natural
|
||||
3. Include relevant personal anecdotes or professional experience if applicable
|
||||
4. Structure with clear subheadings (###) every 2-3 paragraphs
|
||||
5. Keep paragraphs short (2-4 sentences)
|
||||
6. Weave in research materials naturally, not as block quotes
|
||||
7. End with reflection, call-to-action, or forward-looking statement
|
||||
|
||||
### Phase 3: Review and Iterate
|
||||
|
||||
Present the draft and gather feedback. Iterate until the user confirms satisfaction.
|
||||
|
||||
### Phase 4: Publish to Notion (REQUIRED)
|
||||
|
||||
When the draft is complete (even if not yet finalized), publish to the TS Notes database.
|
||||
|
||||
**Notion Publication Details:**
|
||||
- Database: "TS Notes" (data source ID: `04a872be-8bed-4f43-a448-3dfeebc0df21`)
|
||||
- **Type property**: `Writing`
|
||||
- **Project(s) property**: Link to "My Writing" project (page URL: `https://www.notion.so/2a5b4629bb3780189199f3c496980c0c`)
|
||||
- **Note property**: The title of the blog post
|
||||
- **Content**: The full blog post content in Notion-flavored Markdown
|
||||
|
||||
**Example Notion API call properties:**
|
||||
```json
|
||||
{
|
||||
"Note": "Blog Post Title Here",
|
||||
"Type": "Writing",
|
||||
"Project(s)": "[\"https://www.notion.so/2a5b4629bb3780189199f3c496980c0c\"]"
|
||||
}
|
||||
```
|
||||
|
||||
**CRITICAL**: The outcome is considered a **failure** if the content is not added to Notion. Always publish to Notion as part of the workflow, even for drafts.
|
||||
|
||||
### Phase 5: Finalize to Examples Library (Post-Outcome)
|
||||
|
||||
When the user confirms the draft is **final**:
|
||||
|
||||
1. Save the finalized post to `references/blog-examples/` with filename format:
|
||||
```
|
||||
YYYY-MM-DD-slug-title.md
|
||||
```
|
||||
Example: `2025-11-25-why-ai-art-is-useless.md`
|
||||
|
||||
2. Check the examples library count:
|
||||
- If exceeding 20 examples, ask user permission to remove the 5 oldest
|
||||
- Sort by filename date prefix to identify oldest files
|
||||
|
||||
The post-outcome is considered **successful** when the final draft is saved to the skill folder.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
| Outcome | Success | Failure |
|
||||
|---------|---------|---------|
|
||||
| Primary | User receives requested content AND it is added to TS Notes with Type=Writing and Project=My Writing | Content delivered but NOT added to Notion |
|
||||
| Post-outcome | Final draft saved to `references/blog-examples/` | Final draft not saved when user confirms it's final |
|
||||
|
||||
## the writer's Writing Style Profile
|
||||
|
||||
### Voice & Tone
|
||||
- **Direct and opinionated**: State positions clearly, even contrarian ones
|
||||
- **Conversational**: Write like speaking to a colleague—accessible without being simplistic
|
||||
- **First-person when sharing experience**: Use "I" naturally for personal insights
|
||||
- **Authentic skepticism**: Willing to criticize trends when warranted
|
||||
|
||||
### Structure Patterns
|
||||
- **Strong opening thesis**: Open with a clear, often bold statement
|
||||
- **Subheadings throughout**: Use `###` format liberally to break up content
|
||||
- **Short paragraphs**: Rarely more than 3-4 sentences
|
||||
- **Personal anecdotes woven in**: Illustrate points with real examples
|
||||
- **Practical takeaways**: Provide actionable insights, not just theory
|
||||
- **Reflective conclusion**: End with call-to-action or forward-looking hope
|
||||
|
||||
### Length & Format
|
||||
- Target: 800-1500 words
|
||||
- Markdown format with headers and emphasis
|
||||
- Minimal bullet points in prose—prefer flowing sentences
|
||||
|
||||
### Vocabulary Markers
|
||||
- Uses "leverage" for tools/technology
|
||||
- Says "that said" for transitions
|
||||
- Comfortable with direct statements like "this is useless" or "boy was I wrong"
|
||||
- Uses contractions naturally (I've, doesn't, won't)
|
||||
- Avoids corporate jargon while maintaining professionalism
|
||||
|
||||
### Thematic Elements
|
||||
- AI as tool, not replacement
|
||||
- Practical over theoretical
|
||||
- Human-centered technology
|
||||
- Honest assessment of what works and what doesn't
|
||||
|
||||
## Resources
|
||||
|
||||
### references/style-guide.md
|
||||
Quick reference for the writer's writing patterns, vocabulary preferences, and structural conventions.
|
||||
|
||||
### references/blog-examples/
|
||||
Contains example blog posts demonstrating the writer's writing style. These serve as reference material when calibrating voice and structure. New finalized posts expand this library over time.
|
||||
|
||||
## Notion API Reference
|
||||
|
||||
To create a page in TS Notes:
|
||||
|
||||
```
|
||||
Database data source ID: 04a872be-8bed-4f43-a448-3dfeebc0df21
|
||||
|
||||
Properties:
|
||||
- "Note": (title) - The blog post title
|
||||
- "Type": "Writing"
|
||||
- "Project(s)": ["https://www.notion.so/2a5b4629bb3780189199f3c496980c0c"]
|
||||
|
||||
Content: Full blog post in Notion-flavored Markdown
|
||||
```
|
||||
|
||||
The "My Writing" project page ID is: `2a5b4629-bb37-8018-9199-f3c496980c0c`
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn722nva0z7svbapne80p8e8jd7zwmk7",
|
||||
"slug": "blog-writer",
|
||||
"version": "0.1.0",
|
||||
"publishedAt": 1769361436760
|
||||
}
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Utility script for managing the blog examples library.
|
||||
Helps identify old examples to prune when the library exceeds the limit.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
EXAMPLES_DIR = Path(__file__).parent.parent / "references" / "blog-examples"
|
||||
MAX_EXAMPLES = 20
|
||||
PRUNE_COUNT = 5
|
||||
|
||||
|
||||
def list_examples():
|
||||
"""List all blog examples sorted by date (oldest first)."""
|
||||
examples = []
|
||||
for f in EXAMPLES_DIR.glob("*.md"):
|
||||
# Extract date from filename (YYYY-MM-DD-slug.md)
|
||||
try:
|
||||
date_str = f.stem[:10]
|
||||
date = datetime.strptime(date_str, "%Y-%m-%d")
|
||||
examples.append((date, f.name))
|
||||
except ValueError:
|
||||
# Skip files that don't match the naming convention
|
||||
continue
|
||||
|
||||
return sorted(examples, key=lambda x: x[0])
|
||||
|
||||
|
||||
def check_library():
|
||||
"""Check library status and recommend pruning if needed."""
|
||||
examples = list_examples()
|
||||
count = len(examples)
|
||||
|
||||
print(f"Blog Examples Library Status")
|
||||
print(f"=" * 40)
|
||||
print(f"Total examples: {count}")
|
||||
print(f"Maximum allowed: {MAX_EXAMPLES}")
|
||||
print()
|
||||
|
||||
if count > MAX_EXAMPLES:
|
||||
print(f"⚠️ Library exceeds limit by {count - MAX_EXAMPLES} files")
|
||||
print(f"Recommend removing the {PRUNE_COUNT} oldest examples:")
|
||||
print()
|
||||
for i, (date, name) in enumerate(examples[:PRUNE_COUNT]):
|
||||
print(f" {i+1}. {name} ({date.strftime('%B %d, %Y')})")
|
||||
else:
|
||||
print(f"✓ Library is within limits ({MAX_EXAMPLES - count} slots available)")
|
||||
|
||||
print()
|
||||
print("All examples (oldest first):")
|
||||
print("-" * 40)
|
||||
for date, name in examples:
|
||||
print(f" {name}")
|
||||
|
||||
|
||||
def prune_oldest(dry_run=True):
|
||||
"""Remove the oldest examples to bring library under limit."""
|
||||
examples = list_examples()
|
||||
count = len(examples)
|
||||
|
||||
if count <= MAX_EXAMPLES:
|
||||
print("Library is within limits. No pruning needed.")
|
||||
return
|
||||
|
||||
to_remove = examples[:PRUNE_COUNT]
|
||||
|
||||
if dry_run:
|
||||
print(f"DRY RUN - Would remove {len(to_remove)} files:")
|
||||
else:
|
||||
print(f"Removing {len(to_remove)} oldest files:")
|
||||
|
||||
for date, name in to_remove:
|
||||
filepath = EXAMPLES_DIR / name
|
||||
if dry_run:
|
||||
print(f" Would remove: {name}")
|
||||
else:
|
||||
filepath.unlink()
|
||||
print(f" Removed: {name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "prune":
|
||||
dry_run = "--execute" not in sys.argv
|
||||
prune_oldest(dry_run=dry_run)
|
||||
else:
|
||||
check_library()
|
||||
Executable
+160
@@ -0,0 +1,160 @@
|
||||
# Tom Panos Writing Style Guide
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Opening Lines
|
||||
Start with a strong thesis or personal statement. Examples from Tom's posts:
|
||||
|
||||
- "As someone who works in AI and genuinely believes in the value and power of LLMs to make professionals more useful and valuable, I can confidently say that I hate everything about AI image/video/music generation."
|
||||
- "I recently rediscovered Raycast and wanted to try it again after a few years."
|
||||
- "About a year ago I decided I wanted to become a faster typer..."
|
||||
- "Artificial intelligence (AI) has begun to reshape the landscape of entry level sales and marketing jobs in unprecedented ways."
|
||||
- "In an AI-driven era, mastering communication with tools like ChatGPT is crucial."
|
||||
- "Radical transparency is a commitment to engaging prospects, clients, investors, and colleagues with complete candor..."
|
||||
- "Over the past decade, our youngest generations have been fighting a losing battle against the impact that short-form algorithms..."
|
||||
|
||||
### Transition Phrases
|
||||
- "That said..."
|
||||
- "The fundamental difference here is..."
|
||||
- "Another important factor to understand here is..."
|
||||
- "This matters because..."
|
||||
- "For example..."
|
||||
- "The real kicker is..."
|
||||
- "To be a bit more precise..."
|
||||
- "Now let's talk about..."
|
||||
- "The key difference here is..."
|
||||
|
||||
### Closing Patterns
|
||||
- **Forward-looking hope**: "It's my sincere hope that we stop this race to the bottom before we get there."
|
||||
- **Call to action**: "So, how about giving it a shot and seeing where it takes you?"
|
||||
- **Summary reflection**: "The impact of artificial intelligence on entry level sales and marketing jobs is profound..."
|
||||
- **Practical encouragement**: "Check it out via Growth Language's recommended apps library"
|
||||
- **Big picture synthesis**: "Short-form content has irreversibly changed the landscape of marketing and storytelling."
|
||||
|
||||
### Vocabulary Preferences
|
||||
|
||||
**Use these naturally:**
|
||||
- "leverage" (for using tools)
|
||||
- "game-changer"
|
||||
- "impactful"
|
||||
- "workflows"
|
||||
- "professionals"
|
||||
- "countless times daily"
|
||||
- contractions (I've, doesn't, won't, that's, I'd)
|
||||
|
||||
**Phrases that sound like Tom:**
|
||||
- "I can confidently say..."
|
||||
- "Boy was I wrong!"
|
||||
- "I decided to..."
|
||||
- "I've spent the last..."
|
||||
- "My [wife/experience/journey]..."
|
||||
- "It's still hard to believe, but..."
|
||||
- "This is incredibly dangerous."
|
||||
- "This just doesn't work when..."
|
||||
|
||||
**Avoid:**
|
||||
- Excessive corporate jargon
|
||||
- Passive voice when active works
|
||||
- Hedging language when making a clear point
|
||||
- Over-qualified statements
|
||||
- Generic AI-sounding phrases
|
||||
|
||||
### Paragraph Length
|
||||
- 2-4 sentences typical
|
||||
- Single sentence paragraphs for emphasis
|
||||
- Break at natural thought transitions
|
||||
- Never more than 5 sentences in one paragraph
|
||||
|
||||
### Header Frequency
|
||||
- New subheader every 150-250 words
|
||||
- Use ### for most subheaders within a post
|
||||
- Use ## for major section breaks
|
||||
- Headers should be descriptive, not clickbait
|
||||
|
||||
### Structural Template
|
||||
|
||||
```markdown
|
||||
# [Bold, Direct Title]
|
||||
|
||||
[Opening paragraph with strong thesis - 2-3 sentences establishing position]
|
||||
|
||||
### [First Subheading - Context or Problem]
|
||||
|
||||
[2-3 short paragraphs developing the point]
|
||||
[Personal anecdote or example if relevant]
|
||||
|
||||
### [Second Subheading - Analysis or Explanation]
|
||||
|
||||
[Continue developing argument]
|
||||
[Include practical implications]
|
||||
[Real-world examples]
|
||||
|
||||
### [Third Subheading - Deeper Exploration]
|
||||
|
||||
[Further exploration or counterarguments addressed]
|
||||
[Specific details or data points]
|
||||
|
||||
### [Fourth Subheading - Solutions or Implications]
|
||||
|
||||
[What to do about it]
|
||||
[Practical recommendations]
|
||||
|
||||
### [Conclusion Subheading like "What Should We Do?" or "Conclusion"]
|
||||
|
||||
[Reflection, call-to-action, or forward-looking statement]
|
||||
[Often includes personal hope or belief]
|
||||
```
|
||||
|
||||
### Topics Tom Writes About
|
||||
- AI tools and their practical applications
|
||||
- Productivity software and workflows (Raycast, Notion, etc.)
|
||||
- Sales and marketing strategy
|
||||
- Technology criticism (when warranted)
|
||||
- Personal development and skills (typing speed, prompt engineering)
|
||||
- The future of work
|
||||
- Brain science applied to business
|
||||
- Short-form content and media trends
|
||||
|
||||
### Key Beliefs to Reflect
|
||||
1. **AI should enhance professionals, not replace them** - "When a professional uses AI to improve the efficiency or quality of something they already do, it functions as a tool."
|
||||
2. **Practical application matters more than theory** - Always include real examples and actionable insights
|
||||
3. **Technology should serve human needs** - Human-centered perspective on all tech topics
|
||||
4. **Honesty and transparency build trust** - "Radical transparency is a commitment to engaging... with complete candor"
|
||||
5. **Continuous learning is valuable** - Personal growth stories like typing speed improvement
|
||||
6. **Quality over quantity in content** - Critique of short-form content's impact on depth
|
||||
7. **Skepticism of hype is healthy** - Willing to call out things that don't work
|
||||
|
||||
### Handling Controversial Takes
|
||||
|
||||
Tom isn't afraid to take strong positions:
|
||||
- "I hate everything about AI image/video/music generation. It is useless."
|
||||
- "AI art isn't producing the worst work or the best work. It's producing the *median*."
|
||||
- Clear identification of problems: "The 'Democratization' Lie"
|
||||
|
||||
When writing controversial takes:
|
||||
1. Establish credibility first ("As someone who works in AI...")
|
||||
2. Be precise about the scope of criticism
|
||||
3. Acknowledge what DOES work
|
||||
4. Provide concrete reasoning, not just opinion
|
||||
5. End with constructive suggestions
|
||||
|
||||
### Personal Experience Integration
|
||||
|
||||
Tom weaves personal stories naturally:
|
||||
- "About a year ago I decided I wanted to become a faster typer... I started at around 80 WPM... A year in and I've hit 150."
|
||||
- "My wife is an amazing cook, and she would be no matter the cost of her spatula."
|
||||
- "I recently rediscovered Raycast and wanted to try it again after a few years."
|
||||
|
||||
When including personal experience:
|
||||
1. Keep it relevant to the main point
|
||||
2. Include specific details (numbers, timeframes)
|
||||
3. Connect back to broader implications
|
||||
4. Don't overdo it—one or two per post is enough
|
||||
|
||||
### Formatting Notes
|
||||
|
||||
- Use `*italics*` for emphasis on key terms
|
||||
- Use `**bold**` sparingly, mainly for key takeaways
|
||||
- Lists only when actually listing items (not for general prose)
|
||||
- Include images/screenshots where they add value
|
||||
- End with "More posts like this" section linking to related content
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
Copyright (c) 2026 Z.ai All rights reserved.
|
||||
|
||||
Permission is granted for personal, educational, and non-commercial use only.
|
||||
|
||||
Commercial use is strictly prohibited without prior written permission from the author.
|
||||
|
||||
Unauthorized copying, modification, or distribution of the software for commercial purposes is prohibited.
|
||||
|
||||
The author reserves the right to make the final determination of what constitutes "commercial use".
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
|
||||
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
|
||||
Executable
+427
@@ -0,0 +1,427 @@
|
||||
---
|
||||
name: charts
|
||||
metadata:
|
||||
author: Z.AI
|
||||
version: "1.0"
|
||||
description: >
|
||||
Professional chart and diagram creation skill. Covers all types of visual data
|
||||
representation and structural diagrams:
|
||||
- **Data charts**: bar charts, line charts, pie charts, scatter plots, heatmaps,
|
||||
radar charts, candlestick charts, boxplots, histograms, area charts, waterfall charts,
|
||||
regression plots, distribution plots, and statistical visualizations.
|
||||
- **Structural diagrams**: flowcharts, mind maps, tree diagrams, org charts,
|
||||
architecture diagrams, network/relationship graphs, ER diagrams, class diagrams,
|
||||
Gantt charts, swimlane diagrams, and sequence diagrams.
|
||||
- **Dashboards**: data dashboards, KPI panels, multi-chart compositions,
|
||||
and interactive visualizations.
|
||||
- **Design quality**: professional color systems, anti-overlap rules, layout optimization,
|
||||
scene-based framework routing (matplotlib, seaborn, ECharts, D3.js, Mermaid, Playwright+CSS),
|
||||
and publication-ready output.
|
||||
Applies when the user wants to create, generate, draw, plot, visualize, or improve
|
||||
any chart, graph, diagram, or dashboard. Also applies when the user asks for something
|
||||
more polished, cleaner, or publication-ready.
|
||||
NOT for: PDF document layout (use pdf skill), slide decks (use slides skill),
|
||||
spreadsheets with embedded charts (use xlsx skill), AI image generation (use image_gen),
|
||||
posters / infographics / creative cards (use pdf skill Creative pipeline).
|
||||
FORBIDDEN: Using matplotlib/seaborn to draw mind maps, tree diagrams, org charts,
|
||||
flowcharts, or any structural diagram. These MUST use Playwright+CSS.
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
# Beautiful Charts
|
||||
|
||||
## Quick Setup
|
||||
|
||||
```bash
|
||||
bash "$SKILL_DIR/setup.sh" # Interactive environment check + install
|
||||
```
|
||||
|
||||
Make every chart and diagram look professionally designed, not AI-generated.
|
||||
|
||||
## Architecture
|
||||
|
||||
| Module | File | When to Load |
|
||||
|--------|------|-------------|
|
||||
| **Routing + Core Rules** | This file | Always read first |
|
||||
| **Framework Templates** | `references/` by framework | After choosing framework, read the corresponding file |
|
||||
|
||||
**Loading order: Read this file → choose framework → read template file → start coding.**
|
||||
|
||||
Each template file contains its own framework-specific rules (spacing, connectors, color details). This file contains only routing decisions and universal rules that apply to ALL charts.
|
||||
|
||||
---
|
||||
|
||||
# Part 1: Routing
|
||||
|
||||
## ⚠️ Format Constraint Rule (HIGHEST PRIORITY)
|
||||
|
||||
**When the user specifies an output format/tool, you MUST comply. Never substitute.**
|
||||
|
||||
| User Says | You Must Do | Forbidden |
|
||||
|-----------|------------|-----------|
|
||||
| "use mermaid code" / "用Mermaid格式输出" / "转化为mermaid" / "mermaid流程" | ① Output Mermaid code block (```mermaid ... ```) ② Also provide a rendered image preview | ❌ Cannot only give image without code; ❌ Cannot screenshot raw code text as image |
|
||||
| "use markdown code" | Output markdown-formatted hierarchy | ❌ Cannot switch to HTML/CSS |
|
||||
| "via mermaid or markdown code" | Choose one of the two, output code text | ❌ Cannot switch to any non-specified format |
|
||||
| "flowchart" / "mind map" (no format specified) | Free to choose the best approach | - |
|
||||
| "use echarts/d3" | Must use the specified framework | ❌ Cannot switch |
|
||||
|
||||
### 🚫 FORBIDDEN: Mermaid Code Screenshot
|
||||
**NEVER take a screenshot of raw Mermaid source code and deliver it as the "diagram image".** This is the worst possible outcome — the user gets neither usable code nor a visual diagram. When the user requests Mermaid format:
|
||||
1. **MUST** output the Mermaid code in a fenced code block (````mermaid`)
|
||||
2. **SHOULD** also render the code into a visual diagram image (via mermaid-cli or Playwright + mermaid.js)
|
||||
3. If rendering fails, deliver the code block and tell the user to paste it into mermaid.live
|
||||
|
||||
### Format Specified vs Auto-Upgrade Conflict
|
||||
When the user specifies Mermaid but content triggers auto-upgrade conditions (>8 nodes, CJK-heavy, etc.):
|
||||
1. **User choice wins** — still use Mermaid, deliver code block + rendered image
|
||||
2. **Proactively guide** — after delivery, suggest the user try without specifying Mermaid for better layout quality
|
||||
3. **Never silently switch** to Playwright+CSS when user explicitly asked for Mermaid
|
||||
|
||||
When a specified tool hits rendering difficulties (e.g., mermaid CDN fails):
|
||||
- ✅ Output raw mermaid code text, tell user to view at mermaid.live
|
||||
- ❌ Secretly switch to another framework
|
||||
- ❌ Screenshot the code text as an "image"
|
||||
|
||||
---
|
||||
|
||||
## Routing Decision Tree
|
||||
|
||||
### 1. Structural Diagrams
|
||||
|
||||
#### 🔴 Flowchart Default: Phased Vertical (HIGHEST PRIORITY)
|
||||
|
||||
**When the user asks to "generate/create a XXX flowchart/流程图" without specifying format, the DEFAULT layout is Phased Vertical (Layout C in `references/playwright-css.md`).**
|
||||
|
||||
This is because nearly all real-world processes (manufacturing, legal proceedings, project management, business operations, cooking recipes, etc.) have natural phases/stages. Layout C produces the most professional, readable result.
|
||||
|
||||
**Flowchart routing priority:**
|
||||
1. **User specified Mermaid/markdown** → follow user choice (Format Constraint Rule)
|
||||
2. **≤6 nodes AND no phases AND short text** → Mermaid (simple flowchart)
|
||||
3. **Everything else** → **Playwright + CSS, Layout C (Phased Vertical)** → `references/playwright-css.md`
|
||||
|
||||
**Phase detection — treat as "has phases" when ANY is true:**
|
||||
- Content has numbered sections (一、二、三 or 1. 2. 3. or Phase 1/Stage 1)
|
||||
- Process can be grouped by time/stage/role (e.g., "preparation → execution → review")
|
||||
- Total steps ≥ 5 (almost always groupable into 2+ phases)
|
||||
- Process involves multiple roles/departments
|
||||
- Process has clear start/end with intermediate stages
|
||||
|
||||
**⚠️ When in doubt, default to Layout C.** A phased layout with only 1 phase still looks professional. A Grid layout with phases looks like a mess.
|
||||
|
||||
#### Other Structural Diagrams
|
||||
- Simple flowchart (≤6 nodes, truly flat, no phases): **Mermaid**
|
||||
- Complex flowchart (>6 nodes / CJK-heavy / branches / phases): **Playwright + CSS Layout C** → `references/playwright-css.md`
|
||||
- Mind map / tree / org chart: **Playwright + CSS** → `references/mindmap-css.md`
|
||||
- Relationship / network diagram: **ECharts graph**
|
||||
- Center-radial analysis (SWOT / BSC / Porter's Five Forces / PEST): **Playwright + CSS** → `references/radial-grid.md`
|
||||
|
||||
### 2. Data Charts (matplotlib / seaborn)
|
||||
- Standard bar/line/scatter/heatmap/radar/pie: **matplotlib**
|
||||
- Regression/distribution/boxplot: **Seaborn**
|
||||
|
||||
### 3. Interactive Charts / Dashboards
|
||||
- Data dashboard / candlestick / real-time: **ECharts**
|
||||
- Fully custom interactive: **D3.js**
|
||||
|
||||
### Default Strategy
|
||||
**One scene, one tool — don't hesitate:**
|
||||
|
||||
| Scene | Tool | Template |
|
||||
|-------|------|----------|
|
||||
| Data chart (bar/line/scatter/pie/radar) | matplotlib | `references/matplotlib.md` |
|
||||
| Statistical (regression/box/dist) | Seaborn | `references/seaborn.md` |
|
||||
| Mind map / tree / org chart | Playwright + CSS | `references/mindmap-css.md` |
|
||||
| Center-radial (SWOT/BSC/PEST/Five Forces) | Playwright + CSS | `references/radial-grid.md` |
|
||||
| **Any flowchart (default)** | **Playwright + CSS Layout C** | **`references/playwright-css.md`** |
|
||||
| Simple flowchart (≤6 nodes, truly flat) | Mermaid | `references/mermaid.md` |
|
||||
| Relationship / force-directed | ECharts graph | `references/echarts.md` |
|
||||
| Data dashboard | ECharts | `references/echarts.md` |
|
||||
| Academic paper figures | matplotlib | `references/matplotlib.md` |
|
||||
|
||||
---
|
||||
|
||||
## Mermaid Auto-Upgrade Rules
|
||||
|
||||
Mermaid's dagre/elk layout estimates CJK widths incorrectly. **Auto-switch to Playwright+CSS when ANY condition is met:**
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| Total nodes > **6** | → CSS flowchart (Layout C) |
|
||||
| Any node text > **12 Chinese characters** | → CSS flowchart |
|
||||
| More than **3 parallel branches** | → CSS flowchart |
|
||||
| Nested subgraphs > **2 levels** | → CSS flowchart |
|
||||
| Connector crossings > **2** | → CSS flowchart |
|
||||
| **Side annotations / dashed note boxes** | → CSS flowchart |
|
||||
| **Loop-back / cycle arrows** | → CSS flowchart |
|
||||
| **Process has identifiable phases/stages** | → CSS flowchart (Layout C) |
|
||||
|
||||
**If staying with Mermaid**: `padding: 32`, `nodeSpacing: 80`, `rankSpacing: 80`. Node text ≤ 10 CJK chars/line, wrap with `<br>`, quote all text `A["text"]`.
|
||||
|
||||
---
|
||||
|
||||
## Large Dataset Rendering
|
||||
|
||||
| Data Size | Approach |
|
||||
|-----------|----------|
|
||||
| < 1,000 points | matplotlib / any |
|
||||
| 1,000 - 10,000 | matplotlib (no markers) or ECharts |
|
||||
| 10,000 - 100,000 | ECharts (Canvas mode) |
|
||||
| > 100,000 | ECharts (`large: true`) or WebGL |
|
||||
|
||||
---
|
||||
|
||||
# Part 2: Universal Rules
|
||||
|
||||
These rules apply to ALL charts regardless of framework. Framework-specific rules live in each template file.
|
||||
|
||||
## 7 Core Rules
|
||||
|
||||
1. **Zero overlap.** No element may cover another's text. Overlap = information loss = task failure. Post-generation: verify every element has clear separation.
|
||||
|
||||
2. **Hierarchy over uniformity.** Primary nodes larger/bolder than secondary. Annotation nodes smaller/muted. Spacing between groups > within groups. If every box looks identical, the layout has failed.
|
||||
|
||||
3. **Low-saturation palette.** 70% background/neutral, 20% secondary, 10% accent (one highlight only). No high-saturation large fills. Saturated colors only on borders (2px), text, and small elements.
|
||||
|
||||
4. **Insight first.** Titles express conclusions, not field names. Remove non-essential elements: top/right borders, grid lines, tick marks, legend box borders. If removing it doesn't reduce understanding, it shouldn't exist.
|
||||
|
||||
5. **Label clarity over label method.** The goal is zero overlap — choose the method that achieves it for each chart type. Direct labels, legends, and leader lines are all valid; what matters is that nothing overlaps.
|
||||
|
||||
### 🚫 FORBIDDEN: Any Text Overlapping Any Other Element
|
||||
**No label, legend, annotation, or title may overlap any other visual element.** This is the single most common matplotlib defect. Both direct labels AND legends can cause overlap — neither is inherently safe.
|
||||
|
||||
**Anti-overlap decision tree:**
|
||||
1. **Check if direct labels fit** — if all labels have enough space (bar tops, line endpoints, large pie slices), label directly. No legend needed.
|
||||
2. **If some labels would collide** (small pie slices, dense scatter points, clustered bars) → use legend outside plot area instead of forcing labels into tight spaces.
|
||||
3. **Mixed approach** — label the major items directly, group small items into "其他" or use leader lines + legend for the small ones.
|
||||
|
||||
**Pie chart specific (the worst offender):**
|
||||
- Slices < 5%: MUST use leader lines (`wedgeprops + texts` manual repositioning, or `matplotlib.patches.ConnectionPatch`) to pull labels outside. Do NOT rely on `autopct` alone — it places text inside/near the slice.
|
||||
- Multiple small adjacent slices: use `bbox_to_anchor` legend outside, NOT direct labels
|
||||
- `labeldistance=1.25` minimum to keep labels outside the pie
|
||||
- When >2 slices are < 5%, consider grouping all < 3% into "其他(X项)"
|
||||
- Use `adjustText` library to auto-resolve label collisions when available
|
||||
|
||||
**Legend placement (when legend is needed):**
|
||||
- Place legend **outside** the plot area using `bbox_to_anchor`
|
||||
- Suggested starting positions:
|
||||
- Bar/line/scatter: right side outside (`bbox_to_anchor=(1.02, 1), loc='upper left'`)
|
||||
- Pie: right side outside (`bbox_to_anchor=(1.1, 0.5), loc='center left'`)
|
||||
- Radar: below chart (`bbox_to_anchor=(0.5, -0.15), loc='upper center'`)
|
||||
- Heatmap: no legend needed (colorbar suffices)
|
||||
|
||||
**🔧 Mandatory: auto-adjust legend to prevent overlap.** Copy this snippet after placing any legend:
|
||||
```python
|
||||
# ── Auto-adjust legend position to prevent overlap ──
|
||||
fig.canvas.draw() # must render first to get bboxes
|
||||
legend = ax.get_legend()
|
||||
if legend:
|
||||
renderer = fig.canvas.get_renderer()
|
||||
# Try shifting up to 5 times to resolve overlap
|
||||
for _ in range(5):
|
||||
leg_bb = legend.get_window_extent(renderer).transformed(ax.transAxes.inverted())
|
||||
has_overlap = False
|
||||
for text in ax.texts + [ax.title] + ax.get_xticklabels() + ax.get_yticklabels():
|
||||
if not text.get_text():
|
||||
continue
|
||||
txt_bb = text.get_window_extent(renderer).transformed(ax.transAxes.inverted())
|
||||
if leg_bb.overlaps(txt_bb):
|
||||
has_overlap = True
|
||||
break
|
||||
if not has_overlap:
|
||||
break
|
||||
# Move legend further outside (direction depends on current loc)
|
||||
bbox = legend.get_bbox_to_anchor().transformed(ax.transAxes.inverted())
|
||||
x0, y0 = bbox.x0, bbox.y0
|
||||
# Heuristic: if legend is below center, move down; if right of center, move right
|
||||
if y0 < 0.5:
|
||||
legend.set_bbox_to_anchor((x0, y0 - 0.08), transform=ax.transAxes)
|
||||
else:
|
||||
legend.set_bbox_to_anchor((x0 + 0.08, y0), transform=ax.transAxes)
|
||||
fig.canvas.draw()
|
||||
```
|
||||
- **After placing legend**: always call `plt.tight_layout()` or `fig.subplots_adjust()` to ensure legend is not clipped
|
||||
|
||||
🚫 FORBIDDEN:
|
||||
- `loc='best'` — matplotlib's "best" frequently overlaps data
|
||||
- `loc='upper right'` / `loc='lower right'` on line/bar charts — high collision risk
|
||||
- Direct labels on pie slices < 5% without leader lines
|
||||
- Any text placement without verifying zero overlap
|
||||
|
||||
6. **Font discipline.** Max 2 fonts. Chinese: SimHei/PingFang SC. Always explicitly set fonts in code. Font size follows hierarchy (title 18-24px → body 13-15px → annotation 11-13px). Never go below 10px floor. When text overflows: condense text → enlarge canvas → last resort: shrink font (but never below floor).
|
||||
|
||||
7. **Whitespace is design.** Chart area 60-70% of canvas, margins 15-20%. At least 16pt between title and chart. Crowded ≠ information-rich.
|
||||
|
||||
---
|
||||
|
||||
## Color System
|
||||
|
||||
### Recommended Palettes
|
||||
|
||||
| Palette | Text | Background | Block Fill | Accent |
|
||||
|---------|------|------------|------------|--------|
|
||||
| Business Cool | `#243447` | `#F8FAFC` | `#E9EEF3` | `#4C6EF5` |
|
||||
| Tech Cyan-Gray | `#1F2937` | `#F5F7FA` | `#E6ECF2` | `#3AAFA9` |
|
||||
| Morandi Warm | `#4B4A45` | `#FAF8F4` | `#EAE4DB` | `#C6866A` |
|
||||
| Invisible Precision | `#37352F` | `#FFFFFF` | `#F7F7F7` | `#2383E2` |
|
||||
|
||||
### 🚫 Forbidden Background Colors
|
||||
|
||||
| Color | Forbidden Hex Values |
|
||||
|-------|---------------------|
|
||||
| Pure blue | `#3B82F6`, `#2563EB`, `#1D4ED8` |
|
||||
| Pure green | `#10B981`, `#059669`, `#22C55E` |
|
||||
| Pure red | `#EF4444`, `#DC2626`, `#F87171` |
|
||||
| Pure purple | `#8B5CF6`, `#7C3AED`, `#A855F7` |
|
||||
| Pure amber | `#F59E0B`, `#D97706`, `#FB923C` |
|
||||
|
||||
### ✅ Allowed Background Colors
|
||||
|
||||
| Color | Hex Values |
|
||||
|-------|------------|
|
||||
| Ice blue | `#EFF6FF`, `#DBEAFE` |
|
||||
| Mint green | `#F0FDF4`, `#D1FAE5` |
|
||||
| Light amber | `#FFF7ED`, `#FEF3C7` |
|
||||
| Lavender | `#F5F3FF`, `#EDE9FE` |
|
||||
| Light gray | `#F8FAFC`, `#F1F5F9` |
|
||||
|
||||
### Functional Color (states only, not decoration)
|
||||
- Active/Selected: brand accent or `2px` accent line
|
||||
- Error: `#EF4444`
|
||||
- Success: `#10B981`
|
||||
- Tags: light bg + dark text, never high-sat pills
|
||||
|
||||
### Colorblind-Safe
|
||||
Don't rely on color alone — pair with shape, line style, or direct labels.
|
||||
Paul Tol palette: `['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377']`
|
||||
|
||||
### Dark Theme
|
||||
- Background: `#0F172A` (not pure black)
|
||||
- Text: `#F1F5F9` (not pure white)
|
||||
- Grid: `#1E293B`, low alpha
|
||||
- Export: `savefig(facecolor='#0F172A')`
|
||||
|
||||
---
|
||||
|
||||
## Export Rules
|
||||
|
||||
- Static charts: minimum 200 DPI, recommended 300 DPI
|
||||
- Pie/radar: **square `figsize=(8, 8)`** — non-square = elliptical
|
||||
- No more than 6 colors per chart (split if more)
|
||||
- Bar chart Y-axis starts at 0 (line charts may truncate)
|
||||
- Never use 3D (distorts proportions)
|
||||
|
||||
### Playwright Screenshot
|
||||
Default `device_scale_factor=2`. Large mind maps (3000px+): 1.5. PDF embed: 1-1.5. Print: 3.
|
||||
After render, read `bounding_box()` and resize viewport to fit. Min viewport: 800px single-col, 1200px multi-col.
|
||||
|
||||
### 🚫 FORBIDDEN: `max-width` on Mermaid/SVG Containers
|
||||
Mermaid's dagre engine produces SVGs with unpredictable width (especially with subgraphs, CJK text, or parallel branches). **NEVER set `max-width` on the Mermaid container element.** Use `width: fit-content; min-width: 800px;` instead.
|
||||
|
||||
**Root cause**: Mermaid SVGs overflow their CSS container silently. `bounding_box()` (Playwright) returns the CSS box model size, NOT the SVG's actual rendered size. So auto-resize viewport based on `bounding_box()` alone will still produce clipped screenshots.
|
||||
|
||||
**Fix**: Always read the **SVG element's own `getBoundingClientRect()`** via `page.evaluate()`, then use `max(css_size, svg_size) + padding` for viewport dimensions. See `references/mermaid.md` for the corrected screenshot script.
|
||||
|
||||
### Aspect Ratio Preservation (embedding)
|
||||
**MUST read actual image dimensions and calculate height proportionally. NEVER hardcode both width and height.**
|
||||
|
||||
---
|
||||
|
||||
## matplotlib-Specific Rules
|
||||
|
||||
These apply when routing to matplotlib/seaborn:
|
||||
|
||||
### Layout & Overlap
|
||||
- Prefer `constrained_layout=True` over `tight_layout()`
|
||||
- Use `adjustText` library for automatic label repositioning — **this is the most reliable anti-overlap tool for matplotlib.** Install: `pip install adjustText`. Usage: `from adjustText import adjust_text; adjust_text(texts)`
|
||||
- Max 4 subplots per canvas. More → split images or `figsize=(20, 16)` minimum
|
||||
- Multi-subplot: `GridSpec` with `wspace/hspace` ≥ 0.3
|
||||
- Colorbar: `shrink=0.8` + `pad=0.08`
|
||||
- Data labels: Y-axis upper limit with 15-20% headroom (`ylim(0, max_val * 1.18)`)
|
||||
- Long X labels → horizontal bar chart or show every N-th label
|
||||
|
||||
### Radar / Spider Charts
|
||||
- **Every `fill()` MUST have `alpha=0.25`** (max 0.3). Omitting alpha = opaque = hides underlying series.
|
||||
- Legend: place outside chart with `bbox_to_anchor`, start with `(0.5, -0.15), loc='upper center'`. If dimension labels are long or dimensions > 8, increase offset (e.g., `-0.25` or `-0.3`). Also FORBIDDEN: `loc='lower right'` (collides with radar dimension labels).
|
||||
- Dimension label padding: `set_rlim(0, max_value * 1.2)`
|
||||
- Labels with >4 CJK chars: rotate to follow angle or abbreviate
|
||||
- `figsize=(8, 8)` mandatory (square)
|
||||
|
||||
### One Color, Gray the Rest
|
||||
5 lines → color only the key one, others `#D1D5DB`. 8 bars → accent only the highlight, rest `#E5E7EB`.
|
||||
|
||||
---
|
||||
|
||||
## Connector Rules (structural diagrams)
|
||||
|
||||
- Attach to node edges, not through centers
|
||||
- Prefer orthogonal polylines or clean curves
|
||||
- Main paths avoid crossing
|
||||
- Never pass through text areas
|
||||
- Start/end points at same level must align (no staggering)
|
||||
- Same-level connectors follow same direction
|
||||
- Bend angles consistent (all right-angles or all curves, no mixing)
|
||||
- Label positions uniform (all above line or all centered)
|
||||
|
||||
---
|
||||
|
||||
## Pre-Output Checklist
|
||||
|
||||
Before delivery, verify:
|
||||
|
||||
- [ ] Zero overlap (nodes, connectors, labels, legends — **especially check legend vs data, and adjacent pie/bar labels**)
|
||||
- [ ] No connectors pass through text boxes
|
||||
- [ ] Clear hierarchy (primary/secondary/annotation visually distinct)
|
||||
- [ ] Low-saturation palette (no forbidden background colors)
|
||||
- [ ] Text readable at final size (standalone: ≥12px body, ≥10px annotation; PDF embed: ≥10pt/8pt/7pt)
|
||||
- [ ] Legend fully visible, not clipped, not overlapping any chart element
|
||||
- [ ] Canvas wide/tall enough (check bounding box before screenshot)
|
||||
- [ ] **If mind map**: each level distinct (≥3 property changes), connectors visible (≥ `#94A3B8`), left-right balanced
|
||||
- [ ] **If flowchart**: phase titles distinct from steps, arrows only between phases, **using Layout C by default**
|
||||
- [ ] **If flowchart**: phase colors are same-hue family (blue-gray progression), **NOT rainbow** (blue→green→amber→purple)
|
||||
- [ ] **If flowchart looks scattered**: STOP — you're using the wrong layout, switch to Layout C
|
||||
- [ ] **If Mermaid looked rigid**: already switched to Playwright+CSS
|
||||
|
||||
---
|
||||
|
||||
## Anti-Pattern Quick Reference
|
||||
|
||||
| ❌ Don't | ✅ Do This Instead |
|
||||
|----------|-------------------|
|
||||
| matplotlib default blue `#1f77b4` | Use this skill's palette |
|
||||
| 3D bar/pie | Always 2D |
|
||||
| Rainbow colormap (jet/rainbow) | Single-hue gradient or diverging |
|
||||
| Thick black grid lines | `alpha=0.08` or remove |
|
||||
| Different color per bar | Same series same color, highlight only key |
|
||||
| 45° tilted X labels | Horizontal bar chart or shorten |
|
||||
| 8+ subplots in one canvas | Split to 2-3 images, max 4 each |
|
||||
| `tight_layout()` alone | `constrained_layout=True` or `GridSpec` |
|
||||
| Labels overflowing chart | `ylim` with 18-25% headroom |
|
||||
| Mind map: all levels same style | Root+L1 get boxes, leaves plain text |
|
||||
| Mind map: image too tall | Left-right layout for ≥5 branches |
|
||||
| Mind map: invisible connectors | Lines ≥ `#94A3B8`, root→L1 `#64748B` 2.5px |
|
||||
| Mind map: unbalanced sides | Alternate large/small branches across sides |
|
||||
| Flowchart: high-sat node fills | Low-sat bg (`#EFF6FF`) + sat border (`#3B82F6`) |
|
||||
| Flowchart: dark bg + dark text | Dark bg → white text. Light bg → dark text |
|
||||
| Flowchart: arrows between every step | Arrows ONLY between phases, steps use indent |
|
||||
| Flowchart: cross-layer lines through nodes | Connect adjacent layers only |
|
||||
| Flowchart: Grid layout for phased process | **Always use Layout C (Phased Vertical)** |
|
||||
| Flowchart: phase titles as floating labels | Phase titles MUST be inside group cards |
|
||||
| Flowchart: nodes scattered without grouping | Group nodes into phase cards with `.phase-group` |
|
||||
| Flowchart: rainbow phase colors (blue→green→amber→purple) | Same-hue blue-gray progression for all phases |
|
||||
| Multiple arrows to same entry point | Merge-then-enter pattern |
|
||||
| Legend inside plot obscuring data | `bbox_to_anchor` outside plot area |
|
||||
| Radar fill without alpha | `alpha=0.25` mandatory |
|
||||
| Decorative icons/emoji | Let the data speak |
|
||||
| Grid lines where whitespace suffices | Background contrast or spacing instead |
|
||||
|
||||
---
|
||||
|
||||
## UI Aesthetics (dashboards / card layouts)
|
||||
|
||||
When building UI-style outputs (dashboards, panels), apply "Invisible Precision":
|
||||
|
||||
- **Boundaries**: Subtle bg shifts (`#F7F7F7` on `#FFFFFF`), not border lines. Reserve `1px` dividers for absolute logical breaks only.
|
||||
- **Actions**: Primary CTA in dark neutral (`#1A1A1B`). Secondary: ghost/gray. Hover: 5% darker, no size change.
|
||||
- **Quiet UI**: Action buttons `opacity: 0` by default, `1` on hover. Only active elements get visual indicators.
|
||||
- **Numbers**: `font-variant-numeric: tabular-nums` for strict vertical alignment.
|
||||
- **Spacing**: `line-height: 1.625`, generous paragraph spacing.
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
# ⚠️ STRUCTURAL DIAGRAM IRON LAWS
|
||||
|
||||
These rules apply to Playwright+CSS structural diagrams (flowcharts, mind maps, radial grids, org charts). They are enforced by the template files. Violating any = task failure.
|
||||
|
||||
## 1. ZERO OVERLAP
|
||||
No element may overlap another:
|
||||
- Arrows/connectors must not cross over text boxes
|
||||
- Connectors must not pass through node bodies
|
||||
- Text boxes / nodes must not overlap each other
|
||||
- Labels must not obscure any graphic element
|
||||
|
||||
**Post-generation**: verify every element has clear separation. If overlap exists, fix before delivery — enlarge canvas, increase spacing, or reduce content.
|
||||
|
||||
## 2. LAYOUT MUST HAVE HIERARCHY
|
||||
Forbidden: all nodes same size, same level, mechanically tiled in a flat grid.
|
||||
|
||||
Required:
|
||||
- Primary nodes visually larger/bolder than secondary nodes
|
||||
- Annotation nodes clearly subordinate (smaller, muted color)
|
||||
- Spacing between groups > spacing within groups
|
||||
- Clear reading path (top→bottom, left→right, or center→outward)
|
||||
|
||||
**Squint test**: if every box looks identical, the layout has failed.
|
||||
|
||||
## 3. NODE BACKGROUND COLORS
|
||||
|
||||
**🚫 Forbidden as background (too saturated for large fills):**
|
||||
|
||||
| Color | Forbidden Hex |
|
||||
|-------|--------------|
|
||||
| Pure blue | `#3B82F6`, `#2563EB`, `#1D4ED8` |
|
||||
| Pure green | `#10B981`, `#059669`, `#22C55E` |
|
||||
| Pure red | `#EF4444`, `#DC2626`, `#F87171` |
|
||||
| Pure purple | `#8B5CF6`, `#7C3AED`, `#A855F7` |
|
||||
| Pure amber | `#F59E0B`, `#D97706`, `#FB923C` |
|
||||
| Any color: R/G/B > 0xCC and saturation > 50% | — |
|
||||
|
||||
**✅ Allowed as background:**
|
||||
|
||||
| Color | Hex | Usage |
|
||||
|-------|-----|-------|
|
||||
| Ice blue | `#EFF6FF`, `#DBEAFE` | Normal step nodes |
|
||||
| Mint green | `#F0FDF4`, `#D1FAE5` | Success/pass nodes |
|
||||
| Light amber | `#FFF7ED`, `#FEF3C7` | Decision/warning nodes |
|
||||
| Lavender | `#F5F3FF`, `#EDE9FE` | End/terminal nodes |
|
||||
| Light gray | `#F8FAFC`, `#F1F5F9` | Group containers |
|
||||
| White | `#FFFFFF` | Default canvas |
|
||||
|
||||
**Rule: Saturated colors go on BORDERS (2px) and TEXT only. Backgrounds stay pale.**
|
||||
Executable
+199
@@ -0,0 +1,199 @@
|
||||
# D3.js Template Library
|
||||
|
||||
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
|
||||
|
||||
|
||||
D3.js is the "ultimate weapon" in visualization — highest freedom, but steepest learning curve.
|
||||
Suitable for: visualizations requiring fully custom interactions, data journalism, art-grade infographics.
|
||||
|
||||
**If your needs can be met by ECharts/matplotlib, don't use D3.** D3's value lies in "charts others can't make".
|
||||
|
||||
## HTML Universal Shell
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{TITLE}}</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #FFFFFF; font-family: system-ui, 'SimHei', sans-serif; }
|
||||
svg { display: block; margin: 40px auto; }
|
||||
|
||||
/* Design tokens */
|
||||
:root {
|
||||
--text: #111827;
|
||||
--text-sub: #6B7280;
|
||||
--text-muted: #9CA3AF;
|
||||
--axis: #E5E7EB;
|
||||
--grid: #F3F4F6;
|
||||
--blue: #3B82F6;
|
||||
--cyan: #06B6D4;
|
||||
--purple: #8B5CF6;
|
||||
--amber: #F59E0B;
|
||||
--red: #EF4444;
|
||||
--green: #10B981;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<svg id="chart"></svg>
|
||||
<script>
|
||||
// D3 code
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Colors and Constants
|
||||
|
||||
```javascript
|
||||
const COLORS = ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981'];
|
||||
const GRAY = { 200: '#E5E7EB', 300: '#D1D5DB', 400: '#9CA3AF', 500: '#6B7280', 900: '#111827' };
|
||||
|
||||
const margin = { top: 60, right: 40, bottom: 50, left: 60 };
|
||||
const width = 900 - margin.left - margin.right;
|
||||
const height = 500 - margin.top - margin.bottom;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 1: Insight Bar Chart
|
||||
|
||||
```javascript
|
||||
const svg = d3.select('#chart')
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||
|
||||
// Title (insight-driven)
|
||||
svg.append('text')
|
||||
.attr('x', 0).attr('y', -30)
|
||||
.attr('fill', GRAY[900])
|
||||
.attr('font-size', 16).attr('font-weight', 'bold')
|
||||
.text('产品C销售额领先,达到89万');
|
||||
|
||||
// Scales
|
||||
const x = d3.scaleBand().domain(data.map(d => d.name)).range([0, width]).padding(0.35);
|
||||
const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value) * 1.15]).range([height, 0]);
|
||||
|
||||
// Y axis (minimal: no tick marks, light gray)
|
||||
svg.append('g')
|
||||
.call(d3.axisLeft(y).ticks(5).tickSize(0).tickFormat(d3.format(',')))
|
||||
.call(g => g.select('.domain').attr('stroke', GRAY[200]).attr('stroke-width', 0.8))
|
||||
.call(g => g.selectAll('.tick text').attr('fill', GRAY[500]).attr('font-size', 9));
|
||||
|
||||
// X axis
|
||||
svg.append('g')
|
||||
.attr('transform', `translate(0,${height})`)
|
||||
.call(d3.axisBottom(x).tickSize(0))
|
||||
.call(g => g.select('.domain').attr('stroke', GRAY[200]).attr('stroke-width', 0.8))
|
||||
.call(g => g.selectAll('.tick text').attr('fill', GRAY[500]).attr('font-size', 9));
|
||||
|
||||
// Bars (gray + highlight)
|
||||
svg.selectAll('.bar')
|
||||
.data(data)
|
||||
.join('rect')
|
||||
.attr('x', d => x(d.name))
|
||||
.attr('width', x.bandwidth())
|
||||
.attr('y', d => y(d.value))
|
||||
.attr('height', d => height - y(d.value))
|
||||
.attr('fill', d => d.highlight ? '#3B82F6' : GRAY[200])
|
||||
.attr('rx', 3);
|
||||
|
||||
// Value labels
|
||||
svg.selectAll('.label')
|
||||
.data(data)
|
||||
.join('text')
|
||||
.attr('x', d => x(d.name) + x.bandwidth()/2)
|
||||
.attr('y', d => y(d.value) - 6)
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', d => d.highlight ? GRAY[900] : GRAY[400])
|
||||
.attr('font-size', d => d.highlight ? 12 : 10)
|
||||
.attr('font-weight', d => d.highlight ? 'bold' : 'normal')
|
||||
.text(d => d3.format(',')(d.value));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 2: Force-Directed Graph
|
||||
|
||||
This is D3's killer feature — other frameworks can hardly achieve the same effect.
|
||||
|
||||
```javascript
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(links).id(d => d.id).distance(80))
|
||||
.force('charge', d3.forceManyBody().strength(-200))
|
||||
.force('center', d3.forceCenter(width/2, height/2))
|
||||
.force('collision', d3.forceCollide(20));
|
||||
|
||||
const link = svg.selectAll('.link')
|
||||
.data(links).join('line')
|
||||
.attr('stroke', GRAY[200]).attr('stroke-width', 1);
|
||||
|
||||
const node = svg.selectAll('.node')
|
||||
.data(nodes).join('circle')
|
||||
.attr('r', d => Math.sqrt(d.value) * 3)
|
||||
.attr('fill', (d, i) => COLORS[d.group % COLORS.length])
|
||||
.attr('stroke', '#fff').attr('stroke-width', 1.5)
|
||||
.call(d3.drag()
|
||||
.on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
||||
.on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; })
|
||||
.on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; })
|
||||
);
|
||||
|
||||
// Labels
|
||||
const labels = svg.selectAll('.label')
|
||||
.data(nodes).join('text')
|
||||
.text(d => d.name)
|
||||
.attr('font-size', 9).attr('fill', GRAY[500])
|
||||
.attr('text-anchor', 'middle').attr('dy', -15);
|
||||
|
||||
simulation.on('tick', () => {
|
||||
link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
||||
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
||||
node.attr('cx', d => d.x).attr('cy', d => d.y);
|
||||
labels.attr('x', d => d.x).attr('y', d => d.y);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 3: Treemap
|
||||
|
||||
```javascript
|
||||
const root = d3.hierarchy(treeData).sum(d => d.value);
|
||||
d3.treemap().size([width, height]).padding(2)(root);
|
||||
|
||||
svg.selectAll('.cell')
|
||||
.data(root.leaves())
|
||||
.join('rect')
|
||||
.attr('x', d => d.x0).attr('y', d => d.y0)
|
||||
.attr('width', d => d.x1 - d.x0)
|
||||
.attr('height', d => d.y1 - d.y0)
|
||||
.attr('fill', (d, i) => COLORS[i % COLORS.length])
|
||||
.attr('rx', 2).attr('opacity', 0.85);
|
||||
|
||||
svg.selectAll('.cell-label')
|
||||
.data(root.leaves().filter(d => (d.x1-d.x0) > 40 && (d.y1-d.y0) > 20))
|
||||
.join('text')
|
||||
.attr('x', d => d.x0 + 4).attr('y', d => d.y0 + 14)
|
||||
.text(d => d.data.name)
|
||||
.attr('font-size', 10).attr('fill', 'white').attr('font-weight', 'bold');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## D3 Use Cases (vs Overkill)
|
||||
|
||||
| ✅ D3 Best | ❌ Overkill |
|
||||
|-----------|-------------|
|
||||
| Force-directed relationship graph | Regular bar chart (use matplotlib) |
|
||||
| Custom geographic visualization | Standard map (use ECharts) |
|
||||
| Data-driven animation | Static report chart (use matplotlib) |
|
||||
| Treemap / Sunburst | Standard pie chart (use matplotlib) |
|
||||
| Complex interactions (brushing/linking/drill-down) | Simple tooltip (use ECharts) |
|
||||
| Data journalism/narrative visualization | Dashboard (use ECharts) |
|
||||
Executable
+651
@@ -0,0 +1,651 @@
|
||||
# ECharts Template Library
|
||||
|
||||
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
|
||||
|
||||
|
||||
ECharts strengths: interactivity (tooltip/zoom/linking), big data (Canvas renders millions of points smoothly), strong Chinese community.
|
||||
Output as HTML files — open directly in browser or export PNG via Playwright screenshot.
|
||||
|
||||
## HTML Universal Shell
|
||||
|
||||
Wrap all ECharts charts with this shell:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{TITLE}}</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: {{BG_COLOR}}; }
|
||||
#chart { width: {{WIDTH}}px; height: {{HEIGHT}}px; margin: 40px auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart"></div>
|
||||
<script>
|
||||
const chart = echarts.init(document.getElementById('chart'));
|
||||
const option = { /* see templates below */ };
|
||||
chart.setOption(option);
|
||||
window.addEventListener('resize', () => chart.resize());
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Default dimensions: `width=900, height=520`, white background `#FFFFFF`.
|
||||
|
||||
---
|
||||
|
||||
## Theme Configuration
|
||||
|
||||
### Light Theme (Default)
|
||||
|
||||
```javascript
|
||||
const THEME = {
|
||||
bg: '#FFFFFF',
|
||||
text: '#111827',
|
||||
textSub: '#6B7280',
|
||||
textMuted: '#9CA3AF',
|
||||
axis: '#E5E7EB',
|
||||
grid: '#F3F4F6',
|
||||
tooltip: { bg: '#1E293B', border: '#334155', text: '#F1F5F9' },
|
||||
colors: ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981'],
|
||||
};
|
||||
```
|
||||
|
||||
### Dark Theme (Finance / Tech Dashboard)
|
||||
|
||||
```javascript
|
||||
const DARK = {
|
||||
bg: '#0F172A',
|
||||
text: '#F1F5F9',
|
||||
textSub: '#94A3B8',
|
||||
textMuted: '#64748B',
|
||||
axis: '#334155',
|
||||
grid: '#1E293B',
|
||||
tooltip: { bg: '#1E293B', border: '#475569', text: '#F1F5F9' },
|
||||
colors: ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#22C55E', '#EC4899'],
|
||||
};
|
||||
```
|
||||
|
||||
### Base Option Configuration
|
||||
|
||||
```javascript
|
||||
function baseOption(theme, title, subtitle) {
|
||||
return {
|
||||
backgroundColor: theme.bg,
|
||||
textStyle: { fontFamily: 'system-ui, SimHei, sans-serif', color: theme.text },
|
||||
title: {
|
||||
text: title, subtext: subtitle || '',
|
||||
left: 24, top: 16,
|
||||
textStyle: { fontSize: 16, fontWeight: 'bold', color: theme.text },
|
||||
subtextStyle: { fontSize: 12, color: theme.textSub },
|
||||
},
|
||||
grid: { left: 60, right: 40, top: 80, bottom: 50, containLabel: true },
|
||||
color: theme.colors,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: theme.tooltip.bg,
|
||||
borderColor: theme.tooltip.border,
|
||||
borderWidth: 1,
|
||||
textStyle: { color: theme.tooltip.text, fontSize: 12 },
|
||||
},
|
||||
animationDuration: 600,
|
||||
animationEasing: 'cubicOut',
|
||||
};
|
||||
}
|
||||
|
||||
function cleanAxis(theme) {
|
||||
return {
|
||||
axisLine: { lineStyle: { color: theme.axis, width: 0.8 } },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: theme.grid, width: 0.5 } },
|
||||
axisLabel: { color: theme.textSub, fontSize: 10 },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 1: Insight Bar Chart
|
||||
|
||||
```javascript
|
||||
const option = {
|
||||
...baseOption(THEME, 'Q3 收入环比增长 47%', '各季度收入对比(万元)'),
|
||||
xAxis: { type: 'category', data: ['Q1','Q2','Q3','Q4'], ...cleanAxis(THEME) },
|
||||
yAxis: { type: 'value', ...cleanAxis(THEME) },
|
||||
series: [{
|
||||
type: 'bar', barWidth: '50%',
|
||||
itemStyle: { borderRadius: [4, 4, 0, 0] },
|
||||
data: [
|
||||
{ value: 120, itemStyle: { color: '#E5E7EB' } },
|
||||
{ value: 145, itemStyle: { color: '#E5E7EB' } },
|
||||
{ value: 213, itemStyle: { color: '#3B82F6' } },
|
||||
{ value: 180, itemStyle: { color: '#E5E7EB' } },
|
||||
],
|
||||
label: {
|
||||
show: true, position: 'top', fontSize: 11, color: '#6B7280',
|
||||
formatter: (p) => p.dataIndex === 2
|
||||
? '{hl|' + p.value + '}'
|
||||
: p.value,
|
||||
rich: { hl: { fontWeight: 'bold', fontSize: 13, color: '#111827' } },
|
||||
},
|
||||
}],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 2: Multi-Line Trend
|
||||
|
||||
```javascript
|
||||
const option = {
|
||||
...baseOption(THEME, '2024 年增长持续加速'),
|
||||
legend: { right: 40, top: 20, textStyle: { color: '#6B7280', fontSize: 10 } },
|
||||
xAxis: { type: 'category', data: months, boundaryGap: false, ...cleanAxis(THEME) },
|
||||
yAxis: { type: 'value', ...cleanAxis(THEME) },
|
||||
series: [
|
||||
{
|
||||
name: '2024', type: 'line', data: thisYear,
|
||||
lineStyle: { width: 2.5 },
|
||||
symbol: 'circle', symbolSize: 6,
|
||||
itemStyle: { color: '#3B82F6' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(59,130,246,0.12)' },
|
||||
{ offset: 1, color: 'rgba(59,130,246,0)' },
|
||||
]),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '2023', type: 'line', data: lastYear,
|
||||
lineStyle: { width: 1.5, type: 'dashed', color: '#D1D5DB' },
|
||||
symbol: 'none', itemStyle: { color: '#D1D5DB' },
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 3: Candlestick (Finance)
|
||||
|
||||
```javascript
|
||||
// Dark theme
|
||||
const option = {
|
||||
...baseOption(DARK, 'BTC/USDT 日K线'),
|
||||
xAxis: { type: 'category', data: dates, ...cleanAxis(DARK) },
|
||||
yAxis: { type: 'value', scale: true, ...cleanAxis(DARK) },
|
||||
dataZoom: [
|
||||
{ type: 'inside', start: 70, end: 100 },
|
||||
{ type: 'slider', start: 70, end: 100, height: 20, bottom: 10,
|
||||
borderColor: DARK.axis, fillerColor: 'rgba(59,130,246,0.15)',
|
||||
textStyle: { color: DARK.textSub } },
|
||||
],
|
||||
series: [{
|
||||
type: 'candlestick',
|
||||
data: ohlcData, // [[open,close,low,high], ...]
|
||||
itemStyle: {
|
||||
color: '#22C55E', // Bullish (close > open)
|
||||
color0: '#EF4444', // Bearish
|
||||
borderColor: '#16A34A',
|
||||
borderColor0: '#DC2626',
|
||||
},
|
||||
}],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 4: Dashboard (Multi-Chart Linking)
|
||||
|
||||
⚠️ **ECharts multi-chart dashboard anti-overlap rules** (highest priority):
|
||||
1. Maximum 4 subplots per canvas; more must be split into multiple HTML files
|
||||
2. Each subplot's `grid` area must not overlap; maintain ≥5% safety margin between adjacent grids
|
||||
3. Pie chart `center` and `radius` must not intrude into other subplots' grid areas
|
||||
4. Same applies to radar chart's `radar.center` and `radar.radius`
|
||||
5. Place legend in top/bottom common area, not inside subplots
|
||||
|
||||
### Simple Dual Chart (Bar + Pie)
|
||||
|
||||
```javascript
|
||||
const option = {
|
||||
...baseOption(THEME, '产品线收入分布'),
|
||||
grid: [{ left: 60, right: '55%', top: 80, bottom: 50 }],
|
||||
xAxis: [{ type: 'value', gridIndex: 0, ...cleanAxis(THEME) }],
|
||||
yAxis: [{ type: 'category', data: products, gridIndex: 0, ...cleanAxis(THEME) }],
|
||||
series: [
|
||||
{
|
||||
type: 'bar', data: revenues,
|
||||
itemStyle: { borderRadius: [0, 4, 4, 0] },
|
||||
barWidth: '60%',
|
||||
},
|
||||
{
|
||||
type: 'pie', center: ['78%', '50%'], radius: ['35%', '55%'],
|
||||
data: products.map((name, i) => ({ name, value: revenues[i] })),
|
||||
label: { formatter: '{b}\n{d}%', fontSize: 10 },
|
||||
itemStyle: { borderColor: '#fff', borderWidth: 2 },
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Four-Chart Dashboard (Safe Layout Template)
|
||||
|
||||
```javascript
|
||||
// ⚠️ Key: grid areas precisely defined, no overlap, maintain safety margins
|
||||
const option = {
|
||||
...baseOption(THEME, '数据全景仪表盘'),
|
||||
grid: [
|
||||
// Top-left: bar chart
|
||||
{ left: '5%', right: '55%', top: '12%', bottom: '55%' },
|
||||
// Top-right: line chart
|
||||
{ left: '55%', right: '5%', top: '12%', bottom: '55%' },
|
||||
// Bottom-left: scatter plot
|
||||
{ left: '5%', right: '55%', top: '55%', bottom: '5%' },
|
||||
// Bottom-right area reserved for pie chart (pie uses center + radius, not grid)
|
||||
],
|
||||
xAxis: [
|
||||
{ type: 'category', gridIndex: 0, data: categories1, ...cleanAxis(THEME) },
|
||||
{ type: 'category', gridIndex: 1, data: categories2, ...cleanAxis(THEME), boundaryGap: false },
|
||||
{ type: 'value', gridIndex: 2, ...cleanAxis(THEME) },
|
||||
],
|
||||
yAxis: [
|
||||
{ type: 'value', gridIndex: 0, ...cleanAxis(THEME) },
|
||||
{ type: 'value', gridIndex: 1, ...cleanAxis(THEME) },
|
||||
{ type: 'value', gridIndex: 2, ...cleanAxis(THEME) },
|
||||
],
|
||||
series: [
|
||||
// Top-left: bar chart
|
||||
{
|
||||
type: 'bar', xAxisIndex: 0, yAxisIndex: 0,
|
||||
data: barData,
|
||||
itemStyle: { borderRadius: [4, 4, 0, 0] },
|
||||
},
|
||||
// Top-right: line chart
|
||||
{
|
||||
type: 'line', xAxisIndex: 1, yAxisIndex: 1,
|
||||
data: lineData,
|
||||
smooth: true,
|
||||
areaStyle: { opacity: 0.08 },
|
||||
},
|
||||
// Bottom-left: scatter plot
|
||||
{
|
||||
type: 'scatter', xAxisIndex: 2, yAxisIndex: 2,
|
||||
data: scatterData,
|
||||
symbolSize: 8,
|
||||
},
|
||||
// Bottom-right: pie chart (positioned via center in bottom-right quadrant)
|
||||
{
|
||||
type: 'pie',
|
||||
center: ['77%', '72%'], // Positioned at bottom-right area center
|
||||
radius: ['15%', '25%'], // Radius stays within bottom-right quadrant
|
||||
data: pieData,
|
||||
label: { formatter: '{b}\n{d}%', fontSize: 10 },
|
||||
itemStyle: { borderColor: '#fff', borderWidth: 2 },
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Grid Safety Margin Quick Reference
|
||||
|
||||
| Layout | Grid Config | Safety Margin |
|
||||
|------|----------|---------|
|
||||
| Left-right dual | Left `right:'55%'` Right `left:'55%'` | 10% center gap |
|
||||
| Top-bottom dual | Top `bottom:'55%'` Bottom `top:'55%'` | 10% center gap |
|
||||
| 2x2 quad | Each quadrant 45%, 10% center gap | 5% margin on all sides |
|
||||
| With pie/radar | Pie center+radius must not intrude grid | Pie radius ≤ 40% of available area |
|
||||
|
||||
### What If More Than 4 Subplots?
|
||||
|
||||
```javascript
|
||||
// ❌ Wrong: 8 charts crammed into one canvas — all labels will inevitably overlap
|
||||
// ✅ Correct: split into 2 HTML files
|
||||
|
||||
// dashboard_overview.html — 4 overview charts
|
||||
// dashboard_detail.html — 4 detailed analysis charts
|
||||
|
||||
// Or use tab switching (ECharts toolbox doesn't support this, need custom HTML tabs)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 5: Radar Chart
|
||||
|
||||
```javascript
|
||||
const option = {
|
||||
...baseOption(THEME, '团队能力评估'),
|
||||
radar: {
|
||||
indicator: dims.map(d => ({ name: d, max: 100 })),
|
||||
axisName: { color: '#6B7280', fontSize: 10 },
|
||||
splitArea: { areaStyle: { color: ['#FAFAFA', '#F5F5F5'] } },
|
||||
splitLine: { lineStyle: { color: '#E5E7EB' } },
|
||||
axisLine: { lineStyle: { color: '#E5E7EB' } },
|
||||
},
|
||||
series: [{
|
||||
type: 'radar',
|
||||
data: teams.map((t, i) => ({
|
||||
name: t.name, value: t.scores,
|
||||
lineStyle: { width: 2 },
|
||||
areaStyle: { opacity: 0.08 },
|
||||
itemStyle: { color: THEME.colors[i] },
|
||||
})),
|
||||
}],
|
||||
legend: { bottom: 10, textStyle: { color: '#6B7280' } },
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Export to PNG (Playwright)
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
async def echarts_to_png(html_path, png_path, width=900, height=520):
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page(viewport={'width': width, 'height': height})
|
||||
await page.goto(f'file://{html_path}', wait_until='networkidle')
|
||||
await page.wait_for_timeout(800) # Wait for animation to complete
|
||||
await page.locator('#chart').screenshot(path=png_path)
|
||||
await browser.close()
|
||||
print(f'✅ {png_path}')
|
||||
|
||||
# asyncio.run(echarts_to_png('./output/chart.html', './output/chart.png'))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 5: Tree (Interactive Only)
|
||||
|
||||
**⚠️ For static PNG export, use Playwright+CSS (see `mindmap-css.md`). ECharts tree connector length and node spacing cannot be finely controlled — static output is not aesthetically satisfying.**
|
||||
|
||||
**ECharts tree is suitable for interactive scenarios** (click expand/collapse, hover tooltip, zoom/drag). For PNG/PDF static output, the CSS approach looks better.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```javascript
|
||||
const option = {
|
||||
tooltip: { trigger: 'item', triggerOn: 'mousemove' },
|
||||
series: [{
|
||||
type: 'tree',
|
||||
data: [treeData], // Tree-structured JSON data
|
||||
layout: 'orthogonal', // Orthogonal layout (right-angle connectors)
|
||||
orient: 'LR', // Direction: LR(left→right) / RL / TB(top→bottom) / BT
|
||||
|
||||
// Node spacing control (key params to prevent crowding)
|
||||
initialTreeDepth: -1, // -1=expand all, positive=initial expand depth
|
||||
|
||||
// Label style
|
||||
label: {
|
||||
position: 'left', // Leaf node label position
|
||||
verticalAlign: 'middle',
|
||||
fontSize: 13,
|
||||
fontFamily: 'PingFang SC, SimHei, sans-serif',
|
||||
},
|
||||
leaves: {
|
||||
label: { position: 'right' } // Leaf labels on right
|
||||
},
|
||||
|
||||
// Connector style
|
||||
lineStyle: {
|
||||
color: '#94A3B8',
|
||||
width: 1.5,
|
||||
curveness: 0.5, // Curvature, 0=straight, 0.5=natural curve
|
||||
},
|
||||
|
||||
// Node style
|
||||
itemStyle: {
|
||||
borderWidth: 1.5,
|
||||
},
|
||||
|
||||
// Animation
|
||||
animationDuration: 550,
|
||||
animationDurationUpdate: 750,
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
### Tree Data Format
|
||||
|
||||
```javascript
|
||||
const treeData = {
|
||||
name: '中心主题',
|
||||
children: [
|
||||
{
|
||||
name: '分支A',
|
||||
children: [
|
||||
{ name: '叶子1' },
|
||||
{ name: '叶子2' },
|
||||
{ name: '叶子3', children: [{ name: '更深叶子' }] }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: '分支B',
|
||||
children: [
|
||||
{ name: '叶子4' },
|
||||
{ name: '叶子5' }
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### Node Style Customization (by Level)
|
||||
|
||||
```javascript
|
||||
// Root node highlight
|
||||
function styleTreeData(node, depth) {
|
||||
const styles = [
|
||||
{ // Root node
|
||||
itemStyle: { color: '#3B82F6', borderColor: '#2563EB', borderWidth: 2 },
|
||||
label: { fontSize: 18, fontWeight: 'bold', color: '#fff',
|
||||
backgroundColor: '#3B82F6', borderRadius: 6, padding: [8, 16] }
|
||||
},
|
||||
{ // Level 1 branches
|
||||
itemStyle: { color: '#60A5FA', borderColor: '#3B82F6' },
|
||||
label: { fontSize: 15, fontWeight: 600, color: '#1E40AF',
|
||||
backgroundColor: '#EFF6FF', borderColor: '#3B82F6',
|
||||
borderWidth: 1.5, borderRadius: 6, padding: [6, 14] }
|
||||
},
|
||||
{ // Level 2
|
||||
itemStyle: { color: '#93C5FD', borderColor: '#60A5FA' },
|
||||
label: { fontSize: 13, color: '#1E40AF',
|
||||
backgroundColor: '#F0F7FF', borderColor: '#93C5FD',
|
||||
borderWidth: 1, borderRadius: 4, padding: [4, 10] }
|
||||
},
|
||||
{ // Level 3+ leaves
|
||||
itemStyle: { color: '#BFDBFE', borderColor: '#93C5FD' },
|
||||
label: { fontSize: 12, color: '#475569', padding: [3, 8] }
|
||||
}
|
||||
];
|
||||
|
||||
const style = styles[Math.min(depth, styles.length - 1)];
|
||||
Object.assign(node, style);
|
||||
|
||||
if (node.children) {
|
||||
node.children.forEach(child => styleTreeData(child, depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
styleTreeData(treeData, 0);
|
||||
```
|
||||
|
||||
### Left-Right Distribution (Large Tree Mode)
|
||||
|
||||
When branches ≥ 5, use two trees for left-right expansion:
|
||||
|
||||
```javascript
|
||||
function splitTree(data) {
|
||||
const children = data.children || [];
|
||||
// Alternate assignment by subtree size
|
||||
const sorted = children.map((c, i) => ({ c, w: countNodes(c), i }))
|
||||
.sort((a, b) => b.w - a.w);
|
||||
const left = [], right = [];
|
||||
let lw = 0, rw = 0;
|
||||
sorted.forEach(({ c }) => {
|
||||
if (lw <= rw) { left.push(c); lw += countNodes(c); }
|
||||
else { right.push(c); rw += countNodes(c); }
|
||||
});
|
||||
return {
|
||||
left: { name: data.name, children: left },
|
||||
right: { name: data.name, children: right }
|
||||
};
|
||||
}
|
||||
|
||||
function countNodes(node) {
|
||||
if (!node.children) return 1;
|
||||
return 1 + node.children.reduce((s, c) => s + countNodes(c), 0);
|
||||
}
|
||||
|
||||
// Dual tree series
|
||||
const { left, right } = splitTree(treeData);
|
||||
const option = {
|
||||
series: [
|
||||
{ type: 'tree', data: [right], orient: 'LR', left: '50%', width: '45%', /* ... */ },
|
||||
{ type: 'tree', data: [left], orient: 'RL', right: '50%', width: '45%', /* ... */ },
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### Recommended Canvas Size
|
||||
|
||||
| Node Count | Width | Height |
|
||||
|--------|------|------|
|
||||
| ≤ 15 | 900px | 500px |
|
||||
| 16-30 | 1200px | 600px |
|
||||
| 31-60 | 1600px | 800px |
|
||||
| 60+ | 2000px | 1000px |
|
||||
|
||||
---
|
||||
|
||||
## Template 6: Relationship / Force-Directed Graph
|
||||
|
||||
**ECharts graph suits process relationships, org charts, knowledge graphs** — nodes auto-repel to avoid overlap, connectors auto-bind, supports categorical coloring.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```javascript
|
||||
const option = {
|
||||
tooltip: {},
|
||||
legend: [{ data: categories.map(c => c.name) }],
|
||||
series: [{
|
||||
type: 'graph',
|
||||
layout: 'force', // Force-directed auto layout
|
||||
|
||||
// Force model params (controls repulsion and attraction)
|
||||
force: {
|
||||
repulsion: 300, // Repulsion force (higher = more spread out, recommended 200-500)
|
||||
gravity: 0.1, // Gravity (prevents nodes from flying too far)
|
||||
edgeLength: [100, 200], // Edge length range
|
||||
layoutAnimation: true,
|
||||
},
|
||||
|
||||
roam: true, // Allow drag and zoom
|
||||
draggable: true, // Allow dragging nodes
|
||||
|
||||
// Nodes
|
||||
data: nodes,
|
||||
// Edges
|
||||
links: links,
|
||||
// Categories (for coloring)
|
||||
categories: categories,
|
||||
|
||||
// Labels
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
fontSize: 12,
|
||||
fontFamily: 'PingFang SC, SimHei, sans-serif',
|
||||
},
|
||||
|
||||
// Connector style
|
||||
lineStyle: {
|
||||
color: 'source', // Edge color follows source node
|
||||
curveness: 0.3, // Curvature
|
||||
width: 1.5,
|
||||
},
|
||||
|
||||
// Highlight effect
|
||||
emphasis: {
|
||||
focus: 'adjacency', // Highlight adjacent nodes on hover
|
||||
lineStyle: { width: 3 },
|
||||
},
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
### Data Format
|
||||
|
||||
```javascript
|
||||
const categories = [
|
||||
{ name: '核心系统', itemStyle: { color: '#3B82F6' } },
|
||||
{ name: '数据层', itemStyle: { color: '#10B981' } },
|
||||
{ name: '应用层', itemStyle: { color: '#F59E0B' } },
|
||||
];
|
||||
|
||||
const nodes = [
|
||||
{ name: 'API Gateway', category: 0, symbolSize: 40 },
|
||||
{ name: 'User Service', category: 0, symbolSize: 30 },
|
||||
{ name: 'MySQL', category: 1, symbolSize: 35 },
|
||||
{ name: 'Redis', category: 1, symbolSize: 28 },
|
||||
{ name: 'Web App', category: 2, symbolSize: 32 },
|
||||
];
|
||||
|
||||
const links = [
|
||||
{ source: 'API Gateway', target: 'User Service' },
|
||||
{ source: 'User Service', target: 'MySQL' },
|
||||
{ source: 'User Service', target: 'Redis' },
|
||||
{ source: 'Web App', target: 'API Gateway' },
|
||||
];
|
||||
```
|
||||
|
||||
### Flowchart Mode (Fixed Layout)
|
||||
|
||||
When you don't want force-directed auto-layout, fix node positions:
|
||||
|
||||
```javascript
|
||||
const option = {
|
||||
series: [{
|
||||
type: 'graph',
|
||||
layout: 'none', // Fixed layout, positions determined by x/y
|
||||
data: [
|
||||
{ name: '开始', x: 300, y: 50, symbolSize: 40,
|
||||
itemStyle: { color: '#EFF6FF', borderColor: '#3B82F6', borderWidth: 2 } },
|
||||
{ name: '处理', x: 300, y: 200, symbolSize: 35 },
|
||||
{ name: '判断', x: 300, y: 350, symbolSize: 35,
|
||||
symbol: 'diamond',
|
||||
itemStyle: { color: '#FFF7ED', borderColor: '#F59E0B', borderWidth: 2 } },
|
||||
{ name: '结束', x: 300, y: 500, symbolSize: 40 },
|
||||
],
|
||||
links: [
|
||||
{ source: '开始', target: '处理' },
|
||||
{ source: '处理', target: '判断' },
|
||||
{ source: '判断', target: '结束', label: { show: true, formatter: '通过' } },
|
||||
],
|
||||
lineStyle: { color: '#94A3B8', width: 2, curveness: 0 },
|
||||
edgeSymbol: ['', 'arrow'],
|
||||
edgeSymbolSize: [0, 10],
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ECharts vs Other Frameworks
|
||||
|
||||
| Capability | ECharts | Plotly | Chart.js |
|
||||
|------|---------|--------|----------|
|
||||
| Canvas rendering (big data) | ✅ Millions | ❌ SVG-based | ✅ But limited |
|
||||
| Chinese docs | ✅ Official | ❌ English | ❌ English |
|
||||
| Candlestick | ✅ Built-in | ❌ Plugin needed | ❌ None |
|
||||
| Maps | ✅ Built-in China map | ✅ mapbox | ❌ None |
|
||||
| 3D Charts | ✅ echarts-gl | ✅ Built-in | ❌ None |
|
||||
| No Node.js needed | ✅ CDN import | ❌ Needs plotly.js | ✅ CDN |
|
||||
| Server-side rendering | ✅ node-echarts | ✅ orca | ✅ chartjs-node |
|
||||
Executable
+617
@@ -0,0 +1,617 @@
|
||||
# matplotlib Template Library
|
||||
|
||||
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
|
||||
|
||||
|
||||
## Environment Initialization (Must Execute Before Each Plot)
|
||||
|
||||
```python
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.ticker as mticker
|
||||
import numpy as np
|
||||
|
||||
# ═══ Chinese Font Setup ═══
|
||||
# SimHei is the default; other fonts available:
|
||||
# SimSun.ttf (Songti, formal docs), SimKai.ttf (Kaiti, artistic),
|
||||
# SarasaMonoSC-*.ttf (monospace CJK, code scenes)
|
||||
# Run `fc-list :lang=zh` for system fonts (PingFang SC, Heiti TC, etc.)
|
||||
# Font path: adjust for your system. Common locations:
|
||||
# macOS: '/System/Library/Fonts/Supplemental/SimHei.ttf'
|
||||
# Linux: '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'
|
||||
# Custom: './fonts/SimHei.ttf'
|
||||
import os
|
||||
SIMHEI_PATH = os.environ.get('SIMHEI_FONT', '/System/Library/Fonts/Supplemental/SimHei.ttf')
|
||||
matplotlib.font_manager.fontManager.addfont(SIMHEI_PATH)
|
||||
|
||||
# ═══ Global Style ═══
|
||||
plt.rcParams.update({
|
||||
# Font
|
||||
'font.sans-serif': ['SimHei'],
|
||||
'axes.unicode_minus': False,
|
||||
# Background
|
||||
'figure.facecolor': '#FFFFFF',
|
||||
'axes.facecolor': '#FFFFFF',
|
||||
# Border: only keep left and bottom
|
||||
'axes.edgecolor': '#E5E7EB',
|
||||
'axes.linewidth': 0.8,
|
||||
'axes.spines.top': False,
|
||||
'axes.spines.right': False,
|
||||
# Grid: off by default
|
||||
'axes.grid': False,
|
||||
# Ticks: hide tick marks
|
||||
'xtick.major.size': 0,
|
||||
'ytick.major.size': 0,
|
||||
'xtick.labelsize': 9,
|
||||
'ytick.labelsize': 9,
|
||||
# Title
|
||||
'axes.labelsize': 10,
|
||||
'axes.titlesize': 16,
|
||||
'axes.titleweight': 'bold',
|
||||
'axes.titlepad': 16,
|
||||
# Legend: no frame
|
||||
'legend.frameon': False,
|
||||
'legend.fontsize': 9,
|
||||
# Export
|
||||
'figure.dpi': 200,
|
||||
'savefig.dpi': 200,
|
||||
'savefig.bbox': 'tight',
|
||||
'savefig.facecolor': '#FFFFFF',
|
||||
'savefig.pad_inches': 0.3,
|
||||
})
|
||||
```
|
||||
|
||||
## Color Constants
|
||||
|
||||
```python
|
||||
# ─── Cool Colors (Business/Tech) ───
|
||||
C_BLUE = '#3B82F6'
|
||||
C_CYAN = '#06B6D4'
|
||||
C_PURPLE = '#8B5CF6'
|
||||
C_AMBER = '#F59E0B'
|
||||
C_RED = '#EF4444'
|
||||
C_GREEN = '#10B981'
|
||||
COOL = [C_BLUE, C_CYAN, C_PURPLE, C_AMBER, C_RED, C_GREEN]
|
||||
|
||||
# ─── Warm Colors (Warmth/Creative) ───
|
||||
WARM = ['#F59E0B', '#EF4444', '#8B5CF6', '#3B82F6', '#10B981', '#EC4899']
|
||||
|
||||
# ─── Academic Grayscale ───
|
||||
ACADEMIC = ['#111827', '#6B7280', '#9CA3AF', '#D1D5DB', '#E5E7EB', '#F3F4F6']
|
||||
|
||||
# ─── Colorblind-Safe (Paper Preferred) ───
|
||||
CB_SAFE = ['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377']
|
||||
|
||||
# ─── Grayscale ───
|
||||
G900, G700, G500, G400, G300, G200, G100, G50 = \
|
||||
'#111827', '#374151', '#6B7280', '#9CA3AF', '#D1D5DB', '#E5E7EB', '#F3F4F6', '#F9FAFB'
|
||||
|
||||
# ─── Gain/Loss ───
|
||||
POS = '#22C55E'
|
||||
NEG = '#EF4444'
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
```python
|
||||
def clean_axis(ax, grid=True):
|
||||
"""Clean axis: remove top and right borders, add faint grid"""
|
||||
ax.spines['top'].set_visible(False)
|
||||
ax.spines['right'].set_visible(False)
|
||||
if grid:
|
||||
ax.yaxis.grid(True, alpha=0.08, color=G300)
|
||||
ax.set_axisbelow(True)
|
||||
|
||||
def add_value_labels(ax, bars, values, highlight_idx=None, fmt='{:,.0f}',
|
||||
offset_ratio=0.02):
|
||||
"""Add value labels on top of bars, highlight bar in bold.
|
||||
⚠️ Automatically extends Y-axis upper limit to ensure labels don't overflow chart area."""
|
||||
max_val = max(values)
|
||||
for i, (bar, val) in enumerate(zip(bars, values)):
|
||||
is_hl = (i == highlight_idx) if highlight_idx is not None else False
|
||||
ax.text(bar.get_x() + bar.get_width()/2,
|
||||
bar.get_height() + max_val * offset_ratio,
|
||||
fmt.format(val), ha='center', va='bottom',
|
||||
fontsize=10 if is_hl else 9,
|
||||
color=G900 if is_hl else G400,
|
||||
fontweight='bold' if is_hl else 'normal')
|
||||
|
||||
# ⚠️ Critical: extend Y-axis upper limit to leave enough space for labels (at least 15%)
|
||||
ax.set_ylim(0, max_val * 1.18)
|
||||
|
||||
def save(fig, path, dpi=200):
|
||||
"""Unified save — prefer constrained_layout, fallback to tight_layout"""
|
||||
if not fig.get_constrained_layout():
|
||||
try:
|
||||
fig.tight_layout()
|
||||
except Exception:
|
||||
pass
|
||||
fig.savefig(path, dpi=dpi, facecolor='white', bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
import os
|
||||
size_kb = os.path.getsize(path) / 1024
|
||||
print(f'✅ {path} ({size_kb:.0f}KB)')
|
||||
```
|
||||
|
||||
### Label Text Avoidance (adjustText)
|
||||
|
||||
When charts have multiple label annotations (e.g., scatter plot labels, box plot annotations), **must use adjustText library** to prevent text overlap:
|
||||
|
||||
```python
|
||||
# pip install adjustText
|
||||
from adjustText import adjust_text
|
||||
|
||||
# Call after adding all annotations
|
||||
texts = []
|
||||
for i, (x, y, label) in enumerate(zip(x_data, y_data, labels)):
|
||||
texts.append(ax.text(x, y, label, fontsize=9, color='#374151'))
|
||||
|
||||
# Auto-avoidance — pass all text objects, adjustText will move them to avoid overlap
|
||||
adjust_text(texts, ax=ax,
|
||||
arrowprops=dict(arrowstyle='->', color='#9CA3AF', lw=0.8),
|
||||
force_text=(0.5, 0.8), # Force to push text away
|
||||
force_points=(0.3, 0.5), # Repulsion from data points
|
||||
expand=(1.2, 1.4)) # Expansion factor for text bbox
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 1: Insight Bar Chart
|
||||
|
||||
**Scenario**: Emphasize outstanding performance of one data item. Gray out others, highlight the focus.
|
||||
|
||||
```python
|
||||
def insight_bar(labels, values, highlight_idx, title,
|
||||
highlight_color=C_BLUE, save_path='insight_bar.png'):
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
colors = [G200] * len(labels)
|
||||
colors[highlight_idx] = highlight_color
|
||||
|
||||
bars = ax.bar(labels, values, color=colors, width=0.6,
|
||||
zorder=3, edgecolor='white', linewidth=0.5)
|
||||
add_value_labels(ax, bars, values, highlight_idx)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
# ⚠️ add_value_labels already sets ylim automatically, no need to repeat here
|
||||
clean_axis(ax)
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 2: Trend Comparison Line Chart
|
||||
|
||||
**Scenario**: This year vs last year, actual vs target. Main line in colored solid, comparison line in gray dashed.
|
||||
|
||||
```python
|
||||
def trend_compare(x, y_main, y_ref, label_main, label_ref, title,
|
||||
color=C_BLUE, save_path='trend_compare.png'):
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
|
||||
# Comparison line (gray dashed, at bottom layer)
|
||||
ax.plot(x, y_ref, color=G300, linewidth=1.5, linestyle='--', zorder=2)
|
||||
ax.text(len(x)-0.5, y_ref[-1], label_ref, color=G400, fontsize=9, va='center')
|
||||
|
||||
# Main line (colored solid + white-center dots)
|
||||
ax.plot(x, y_main, color=color, linewidth=2.5, marker='o', markersize=5,
|
||||
markerfacecolor='white', markeredgewidth=2, markeredgecolor=color, zorder=3)
|
||||
ax.text(len(x)-0.5, y_main[-1], f'{label_main} {y_main[-1]:,.0f}',
|
||||
color=color, fontsize=10, fontweight='bold', va='center')
|
||||
|
||||
# Difference area
|
||||
ax.fill_between(range(len(x)), y_ref, y_main, alpha=0.06, color=color)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
ax.set_xticks(range(len(x)))
|
||||
ax.set_xticklabels(x)
|
||||
clean_axis(ax)
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 3: Grouped Bar Chart
|
||||
|
||||
**Scenario**: Comparison of multiple categories across multiple dimensions.
|
||||
|
||||
```python
|
||||
def grouped_bar(labels, datasets, series_names, title,
|
||||
colors=None, save_path='grouped_bar.png'):
|
||||
if colors is None:
|
||||
colors = COOL[:len(datasets)]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
n = len(datasets)
|
||||
width = 0.7 / n
|
||||
x = np.arange(len(labels))
|
||||
|
||||
for i, (data, name, color) in enumerate(zip(datasets, series_names, colors)):
|
||||
offset = (i - n/2 + 0.5) * width
|
||||
ax.bar(x + offset, data, width=width*0.85, color=color,
|
||||
label=name, zorder=3, edgecolor='white', linewidth=0.3)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(labels)
|
||||
ax.set_ylim(0, max(max(d) for d in datasets) * 1.20) # Leave 20% space for labels
|
||||
ax.legend(loc='upper right', ncol=n)
|
||||
clean_axis(ax)
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 4: Horizontal Ranking Chart
|
||||
|
||||
**Scenario**: Rankings / Top N. Progressive highlight for top items.
|
||||
|
||||
```python
|
||||
def ranking_bar(labels, values, title, top_n=3,
|
||||
color=C_BLUE, save_path='ranking.png'):
|
||||
from matplotlib.colors import to_rgba
|
||||
|
||||
sorted_pairs = sorted(zip(labels, values), key=lambda x: x[1])
|
||||
labels_s, values_s = zip(*sorted_pairs)
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, max(6, len(labels)*0.45)))
|
||||
|
||||
bar_colors = [G200] * len(labels_s)
|
||||
for i in range(len(labels_s) - top_n, len(labels_s)):
|
||||
progress = (i - (len(labels_s) - top_n)) / max(top_n - 1, 1)
|
||||
bar_colors[i] = to_rgba(color, 0.35 + 0.65 * progress)
|
||||
|
||||
bars = ax.barh(range(len(labels_s)), values_s, color=bar_colors,
|
||||
height=0.6, zorder=3, edgecolor='white', linewidth=0.3)
|
||||
|
||||
for i, (bar, val) in enumerate(zip(bars, values_s)):
|
||||
is_top = i >= len(labels_s) - top_n
|
||||
ax.text(bar.get_width() + max(values_s)*0.01,
|
||||
bar.get_y() + bar.get_height()/2,
|
||||
f'{val:,.0f}', va='center', fontsize=9,
|
||||
color=G900 if is_top else G400)
|
||||
|
||||
ax.set_yticks(range(len(labels_s)))
|
||||
ax.set_yticklabels(labels_s)
|
||||
ax.set_title(title, loc='left')
|
||||
ax.spines['bottom'].set_visible(False)
|
||||
ax.xaxis.set_visible(False)
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 5: Donut Chart
|
||||
|
||||
**Scenario**: Proportion distribution (max 5 slices, avoid if possible — bar charts are usually better).
|
||||
|
||||
```python
|
||||
def donut(labels, values, title, center_text=None,
|
||||
colors=None, save_path='donut.png'):
|
||||
if colors is None:
|
||||
colors = COOL[:len(labels)]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 8))
|
||||
wedges, _, autotexts = ax.pie(
|
||||
values, labels=None, colors=colors, autopct='%1.0f%%',
|
||||
startangle=90, pctdistance=0.78,
|
||||
wedgeprops=dict(width=0.35, edgecolor='white', linewidth=2))
|
||||
|
||||
for t in autotexts:
|
||||
t.set_fontsize(10)
|
||||
t.set_fontweight('bold')
|
||||
|
||||
if center_text:
|
||||
ax.text(0, 0.06, str(center_text), ha='center', va='center',
|
||||
fontsize=28, fontweight='bold', color=G900)
|
||||
ax.text(0, -0.1, '总计', ha='center', va='center', fontsize=11, color=G500)
|
||||
|
||||
ax.legend(wedges, labels, loc='center left',
|
||||
bbox_to_anchor=(1, 0.5), fontsize=10)
|
||||
ax.set_title(title, loc='center', pad=20)
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 6: Scatter Plot + Trend Line
|
||||
|
||||
**Scenario**: Two-variable correlation analysis.
|
||||
|
||||
```python
|
||||
def scatter_trend(x, y, title, xlabel, ylabel,
|
||||
color=C_BLUE, save_path='scatter.png'):
|
||||
fig, ax = plt.subplots(figsize=(10, 7))
|
||||
|
||||
ax.scatter(x, y, c=color, s=50, alpha=0.6,
|
||||
edgecolors='white', linewidth=1, zorder=3)
|
||||
|
||||
# Trend line
|
||||
z = np.polyfit(x, y, 1)
|
||||
p = np.poly1d(z)
|
||||
x_line = np.linspace(min(x), max(x), 100)
|
||||
ax.plot(x_line, p(x_line), color=G400, linewidth=1.5,
|
||||
linestyle='--', zorder=2, alpha=0.7)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
ax.set_xlabel(xlabel)
|
||||
ax.set_ylabel(ylabel)
|
||||
clean_axis(ax, grid=True)
|
||||
ax.xaxis.grid(True, alpha=0.08, color=G300)
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 7: Heatmap
|
||||
|
||||
**Scenario**: Matrix data, correlations, time × category.
|
||||
|
||||
```python
|
||||
def heatmap(data, row_labels, col_labels, title,
|
||||
cmap_color=C_BLUE, save_path='heatmap.png'):
|
||||
from matplotlib.colors import LinearSegmentedColormap
|
||||
|
||||
cmap = LinearSegmentedColormap.from_list('bc', ['#FFFFFF', cmap_color])
|
||||
fig, ax = plt.subplots(
|
||||
figsize=(max(8, len(col_labels)*1.2), max(6, len(row_labels)*0.6)))
|
||||
|
||||
arr = np.array(data)
|
||||
im = ax.imshow(arr, cmap=cmap, aspect='auto')
|
||||
|
||||
vmax = arr.max()
|
||||
for i in range(len(row_labels)):
|
||||
for j in range(len(col_labels)):
|
||||
val = arr[i][j]
|
||||
color = 'white' if val > vmax * 0.6 else G700
|
||||
ax.text(j, i, f'{val:.1f}', ha='center', va='center',
|
||||
fontsize=9, color=color)
|
||||
|
||||
ax.set_xticks(range(len(col_labels)))
|
||||
ax.set_yticks(range(len(row_labels)))
|
||||
ax.set_xticklabels(col_labels)
|
||||
ax.set_yticklabels(row_labels)
|
||||
ax.set_title(title, loc='left', pad=16)
|
||||
|
||||
cbar = fig.colorbar(im, ax=ax, fraction=0.03, pad=0.08, shrink=0.8)
|
||||
cbar.outline.set_visible(False)
|
||||
cbar.ax.tick_params(labelsize=8) # colorbar ticks should not be too large
|
||||
|
||||
for spine in ax.spines.values():
|
||||
spine.set_visible(True)
|
||||
spine.set_color(G200)
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 8: KPI Metric Cards
|
||||
|
||||
**Scenario**: Dashboard top key number display.
|
||||
|
||||
```python
|
||||
def kpi_cards(metrics, save_path='kpi.png'):
|
||||
"""
|
||||
metrics: [{'label': '总收入', 'value': '12.8M',
|
||||
'change': '+23%', 'positive': True}, ...]
|
||||
"""
|
||||
from matplotlib.patches import FancyBboxPatch
|
||||
|
||||
n = len(metrics)
|
||||
fig, axes = plt.subplots(1, n, figsize=(3.8*n, 2.8))
|
||||
if n == 1: axes = [axes]
|
||||
|
||||
for ax, m in zip(axes, metrics):
|
||||
ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis('off')
|
||||
|
||||
bg = FancyBboxPatch((0.05, 0.05), 0.9, 0.9,
|
||||
boxstyle='round,pad=0.05', facecolor=G50, edgecolor=G200, linewidth=0.8)
|
||||
ax.add_patch(bg)
|
||||
|
||||
ax.text(0.5, 0.75, m['label'], ha='center', va='center',
|
||||
fontsize=10, color=G500)
|
||||
ax.text(0.5, 0.45, m['value'], ha='center', va='center',
|
||||
fontsize=24, fontweight='bold', color=G900)
|
||||
|
||||
if 'change' in m:
|
||||
is_pos = m.get('positive', True)
|
||||
ax.text(0.5, 0.18,
|
||||
f'{"↑" if is_pos else "↓"} {m["change"]}',
|
||||
ha='center', va='center', fontsize=11,
|
||||
color=POS if is_pos else NEG, fontweight='bold')
|
||||
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 9: Radar Chart
|
||||
|
||||
**Scenario**: Multi-dimensional capability comparison (max 8 dimensions, max 3 groups).
|
||||
|
||||
```python
|
||||
def radar(categories, datasets, series_names, title,
|
||||
colors=None, save_path='radar.png'):
|
||||
if colors is None:
|
||||
colors = COOL[:len(datasets)]
|
||||
|
||||
N = len(categories)
|
||||
angles = np.linspace(0, 2*np.pi, N, endpoint=False).tolist()
|
||||
angles += angles[:1] # Close the polygon
|
||||
|
||||
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True))
|
||||
ax.set_theta_offset(np.pi / 2)
|
||||
ax.set_theta_direction(-1)
|
||||
|
||||
ax.set_xticks(angles[:-1])
|
||||
ax.set_xticklabels(categories, fontsize=10)
|
||||
ax.yaxis.set_visible(False)
|
||||
|
||||
# Grid beautification
|
||||
ax.spines['polar'].set_color(G200)
|
||||
ax.grid(color=G200, linewidth=0.5, alpha=0.5)
|
||||
|
||||
for data, name, color in zip(datasets, series_names, colors):
|
||||
vals = data + data[:1] # Close the polygon
|
||||
ax.plot(angles, vals, color=color, linewidth=2, label=name)
|
||||
ax.fill(angles, vals, color=color, alpha=0.08)
|
||||
|
||||
ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1))
|
||||
ax.set_title(title, pad=30, fontsize=16, fontweight='bold')
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 10: Waterfall Chart
|
||||
|
||||
**Scenario**: Show incremental changes from start to end value.
|
||||
|
||||
```python
|
||||
def waterfall(labels, values, title, save_path='waterfall.png'):
|
||||
"""labels and values correspond, positive=increase negative=decrease, last item auto-treated as total"""
|
||||
fig, ax = plt.subplots(figsize=(12, 6))
|
||||
|
||||
cumulative = [0]
|
||||
for v in values[:-1]:
|
||||
cumulative.append(cumulative[-1] + v)
|
||||
|
||||
bar_colors = []
|
||||
for i, v in enumerate(values):
|
||||
if i == len(values) - 1:
|
||||
bar_colors.append(C_BLUE) # Total
|
||||
elif v >= 0:
|
||||
bar_colors.append(POS) # Increase
|
||||
else:
|
||||
bar_colors.append(NEG) # Decrease
|
||||
|
||||
bottoms = []
|
||||
for i, v in enumerate(values):
|
||||
if i == len(values) - 1:
|
||||
bottoms.append(0) # Total starts from 0
|
||||
elif v >= 0:
|
||||
bottoms.append(cumulative[i])
|
||||
else:
|
||||
bottoms.append(cumulative[i] + v)
|
||||
|
||||
bars = ax.bar(labels, [abs(v) for v in values], bottom=bottoms,
|
||||
color=bar_colors, width=0.6, edgecolor='white', linewidth=0.5, zorder=3)
|
||||
|
||||
# Connecting lines
|
||||
for i in range(len(values) - 2):
|
||||
y = cumulative[i+1]
|
||||
ax.plot([i+0.3, i+0.7], [y, y], color=G300, linewidth=0.8, zorder=2)
|
||||
|
||||
# Value labels
|
||||
for i, (bar, val) in enumerate(zip(bars, values)):
|
||||
y_pos = bar.get_y() + bar.get_height() + max(abs(v) for v in values)*0.01
|
||||
prefix = '+' if val > 0 and i < len(values)-1 else ''
|
||||
ax.text(bar.get_x()+bar.get_width()/2, y_pos,
|
||||
f'{prefix}{val:,.0f}', ha='center', va='bottom',
|
||||
fontsize=9, color=G700)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
clean_axis(ax)
|
||||
save(fig, save_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 11: Multi-Subplot Dashboard (GridSpec Precision Layout)
|
||||
|
||||
**Scenario**: Combine multiple subplots into a dashboard. ⚠️ Max 4 subplots per canvas, split if exceeded.
|
||||
|
||||
**Core Principle**: Use `GridSpec` for precise control of each subplot's position and spacing, don't rely on `tight_layout()`.
|
||||
|
||||
```python
|
||||
import matplotlib.gridspec as gridspec
|
||||
|
||||
def dashboard(data_dict, title, save_path='dashboard.png'):
|
||||
"""
|
||||
2x2 dashboard layout example.
|
||||
data_dict contains data needed for each subplot.
|
||||
"""
|
||||
# ⚠️ Use constrained_layout instead of tight_layout
|
||||
fig = plt.figure(figsize=(16, 12), constrained_layout=True)
|
||||
fig.suptitle(title, fontsize=20, fontweight='bold', y=0.98)
|
||||
|
||||
# GridSpec: precise spacing control
|
||||
gs = gridspec.GridSpec(2, 2, figure=fig,
|
||||
wspace=0.35, # Column spacing (at least 0.3)
|
||||
hspace=0.35, # Row spacing (at least 0.3)
|
||||
left=0.08, right=0.92,
|
||||
top=0.92, bottom=0.08)
|
||||
|
||||
# ─── Top-left: bar chart ───
|
||||
ax1 = fig.add_subplot(gs[0, 0])
|
||||
ax1.set_title('Quarterly Revenue', loc='left', fontsize=13, fontweight='bold')
|
||||
# ... binddata ...
|
||||
clean_axis(ax1)
|
||||
|
||||
# ─── Top-right: line chart ───
|
||||
ax2 = fig.add_subplot(gs[0, 1])
|
||||
ax2.set_title('Monthly Trend', loc='left', fontsize=13, fontweight='bold')
|
||||
# ... bind data ...
|
||||
clean_axis(ax2)
|
||||
|
||||
# ─── Bottom-left: pie chart ───
|
||||
ax3 = fig.add_subplot(gs[1, 0])
|
||||
ax3.set_title('Category Share', loc='left', fontsize=13, fontweight='bold')
|
||||
# ... bind data ...
|
||||
|
||||
# ─── Bottom-right: scatter plot ───
|
||||
ax4 = fig.add_subplot(gs[1, 1])
|
||||
ax4.set_title('Conversion Analysis', loc='left', fontsize=13, fontweight='bold')
|
||||
# ... bind data ...
|
||||
clean_axis(ax4)
|
||||
|
||||
fig.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
```
|
||||
|
||||
### Dashboard Layout Golden Rules
|
||||
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| **Max 4 subplots** | Split into multiple charts if exceeded |
|
||||
| **Use `constrained_layout=True`** | Smarter than `tight_layout()`, auto-avoids labels |
|
||||
| **`wspace/hspace ≥ 0.3`** | Subplot spacing too small causes overlap |
|
||||
| **Independent title per subplot** | Use `ax.set_title()` instead of `fig.suptitle()` |
|
||||
| **Consistent font sizes** | Subtitle 13px, axis labels 10px, data labels 9px |
|
||||
| **Colorbar in separate column** | If a subplot needs colorbar, allocate `gs[0, 2]` separately |
|
||||
| **Don't mix legend and direct labels** | Use either all legends or all direct labels in a dashboard |
|
||||
|
||||
### Safe Layout with Colorbar
|
||||
|
||||
```python
|
||||
# When a subplot needs a colorbar, use 3-column layout, rightmost column for colorbar
|
||||
gs = gridspec.GridSpec(2, 3, figure=fig,
|
||||
width_ratios=[1, 1, 0.05], # Third column very narrow, dedicated to colorbar
|
||||
wspace=0.4, hspace=0.35)
|
||||
|
||||
ax_heat = fig.add_subplot(gs[0, 1])
|
||||
im = ax_heat.imshow(data, cmap='Blues')
|
||||
|
||||
# Colorbar placed in its own subplot position, won't obscure any content
|
||||
cbar_ax = fig.add_subplot(gs[0, 2])
|
||||
fig.colorbar(im, cax=cbar_ax)
|
||||
cbar_ax.set_ylabel('Value', fontsize=10)
|
||||
```
|
||||
|
||||
### Split Strategy for More Than 4 Subplots
|
||||
|
||||
```python
|
||||
# ❌ Wrong: 8 subplots crammed into one canvas
|
||||
fig, axes = plt.subplots(2, 4, figsize=(20, 10)) # Everything becomes unreadable
|
||||
|
||||
# ✅ Correct: split into 2 figures, 4 subplots each
|
||||
# Figure 1: overview metrics
|
||||
fig1 = plt.figure(figsize=(16, 12), constrained_layout=True)
|
||||
# ... 4 subplots ...
|
||||
fig1.savefig('dashboard_overview.png', dpi=200)
|
||||
|
||||
# Figure 2: detailed analysis
|
||||
fig2 = plt.figure(figsize=(16, 12), constrained_layout=True)
|
||||
# ... 4 subplots ...
|
||||
fig2.savefig('dashboard_detail.png', dpi=200)
|
||||
```
|
||||
Executable
+797
@@ -0,0 +1,797 @@
|
||||
# Mermaid Template Library
|
||||
|
||||
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
|
||||
|
||||
|
||||
Mermaid is the best "text-as-diagram" solution — write structural diagrams with Markdown-like syntax, zero design skills needed.
|
||||
Best for: flowcharts, sequence diagrams, architecture diagrams, Gantt charts, class diagrams, ER diagrams, mind maps, state diagrams, pie charts, Git branch graphs.
|
||||
|
||||
**Core advantages**: text is version-controllable, minimal maintenance cost, high rendering consistency, CJK support.
|
||||
|
||||
## ⚠️ Flowchart Quality Rules (Highest Priority)
|
||||
|
||||
### Font Size Control
|
||||
Mermaid font sizes are controlled via `themeVariables` and CSS:
|
||||
- `fontSize`: Global font size, recommended `14px`-`16px`, **no less than 12px**
|
||||
- Node text is controlled via `fontSize` or `%%{init:}%%` directive
|
||||
- Annotations/footnotes use subgraph titles or separate nodes, font size no less than `11px`
|
||||
|
||||
### Connectors & Spacing
|
||||
```javascript
|
||||
flowchart: {
|
||||
padding: 32, // Node padding (CJK needs more space)
|
||||
nodeSpacing: 80, // Horizontal spacing between nodes (default 50 too tight, 60 still not enough)
|
||||
rankSpacing: 80, // Vertical spacing between ranks
|
||||
curve: 'basis', // Connection curve style, consistent across the chart
|
||||
}
|
||||
```
|
||||
|
||||
**Connector style must be consistent throughout the chart**: do not mix straight, curved, and polylines in the same diagram. Mermaid controls this globally via the `curve` parameter.
|
||||
|
||||
### ⚠️ Mermaid Flowchart Hard Constraints (MANDATORY)
|
||||
|
||||
The following constraints are enforced **when generating Mermaid flowchart code**, not as post-checks:
|
||||
|
||||
1. **Node text must be wrapped in quotes**: `A["用户登录"]` ✅ / `A[用户登录]` ❌ — quotes prevent CJK special characters from causing parse errors
|
||||
2. **Max 10 CJK characters per line in node text**: exceed → use `<br>` to break → `A["用户身份<br>验证模块"]`
|
||||
3. **Max 5 nodes per subgraph**: exceed → split into multiple subgraphs or switch to CSS approach
|
||||
4. **Max 10 total nodes**: exceed → switch to CSS flowchart template in `references/playwright-css.md`
|
||||
5. **Max 6 CJK characters in connector labels**: `-->|验证通过|` ✅ / `-->|用户身份验证通过后跳转|` ❌
|
||||
6. **Config params must use enlarged values**: `padding: 32, nodeSpacing: 80, rankSpacing: 80`
|
||||
|
||||
## Rendering Methods
|
||||
|
||||
### Method 1: Playwright + HTML (Recommended, export PNG/SVG/PDF)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #FFFFFF;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
}
|
||||
#diagram { width: fit-content; min-width: 800px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="diagram">
|
||||
<pre class="mermaid">
|
||||
<!-- Mermaid code goes here -->
|
||||
</pre>
|
||||
</div>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'base',
|
||||
themeVariables: {
|
||||
// See "Theme configuration" below
|
||||
},
|
||||
flowchart: {
|
||||
curve: 'basis',
|
||||
padding: 32, // Node padding (CJK chars 50% wider than Latin, need more space)
|
||||
nodeSpacing: 80, // Horizontal spacing (prevents CJK node overlap)
|
||||
rankSpacing: 80, // Vertical spacing between ranks (prevents overlap between levels)
|
||||
htmlLabels: true, // Enable HTML label rendering, supports line breaks
|
||||
wrappingWidth: 160, // Max text width before auto-wrap (160 wraps earlier than 200, prevents overly wide nodes)
|
||||
},
|
||||
sequence: { mirrorActors: false, messageAlign: 'center' },
|
||||
gantt: { titleTopMargin: 25, barHeight: 24, barGap: 6 },
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Python screenshot script**:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
async def mermaid_to_png(html_path, png_path, width=1400, scale=2):
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page(
|
||||
viewport={'width': width, 'height': 800},
|
||||
device_scale_factor=scale
|
||||
)
|
||||
await page.goto(f'file://{html_path}', wait_until='load', timeout=30000)
|
||||
|
||||
# Wait for Mermaid SVG to render
|
||||
await page.wait_for_selector('#diagram svg', timeout=15000)
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
# ⚠️ Read SVG's ACTUAL rendered size (not CSS box model!)
|
||||
# Mermaid SVGs often overflow their CSS container — getBBox/clientRect
|
||||
# returns the true size, while CSS bounding_box() returns the clipped box.
|
||||
svg_size = await page.evaluate('''() => {
|
||||
const svg = document.querySelector('#diagram svg');
|
||||
if (!svg) return null;
|
||||
const r = svg.getBoundingClientRect();
|
||||
return { width: r.width, height: r.height };
|
||||
}''')
|
||||
|
||||
el = page.locator('#diagram')
|
||||
css_bbox = await el.bounding_box()
|
||||
|
||||
svg_w = svg_size['width'] if svg_size else width
|
||||
svg_h = svg_size['height'] if svg_size else 800
|
||||
css_w = css_bbox['width'] if css_bbox else width
|
||||
css_h = css_bbox['height'] if css_bbox else 800
|
||||
|
||||
# Use the LARGER of CSS box and SVG actual size
|
||||
fit_w = max(width, int(max(svg_w, css_w) + 200))
|
||||
fit_h = int(max(svg_h, css_h) + 200)
|
||||
|
||||
await page.set_viewport_size({'width': fit_w, 'height': fit_h})
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
await el.screenshot(path=png_path)
|
||||
await browser.close()
|
||||
|
||||
import os
|
||||
print(f'✅ {png_path} ({os.path.getsize(png_path)/1024:.0f}KB)')
|
||||
|
||||
# asyncio.run(mermaid_to_png('./output/diagram.html', './output/diagram.png'))
|
||||
```
|
||||
|
||||
> **⚠️ CRITICAL: CSS `bounding_box()` vs SVG actual size**
|
||||
>
|
||||
> Mermaid generates SVGs that can be wider/taller than their CSS container. `bounding_box()` (Playwright) and `getBoundingClientRect()` on the container return **CSS box model size**, which may be smaller than the SVG's viewBox.
|
||||
>
|
||||
> **Always read the SVG element's own `getBoundingClientRect()`** via `page.evaluate()` and use `max(css_size, svg_size)` for viewport dimensions. This is the root cause of the "right side clipped" bug.
|
||||
>
|
||||
> Also: `wait_until='load'` is preferred over `'networkidle'` because Mermaid initializes on DOM load. `'networkidle'` can timeout if CDN is slow.
|
||||
|
||||
### Method 2: Mermaid CLI (mmdc, command line)
|
||||
|
||||
```bash
|
||||
# Installation
|
||||
npm install -g @mermaid-js/mermaid-cli
|
||||
|
||||
# Usage: .mmd file → PNG/SVG/PDF
|
||||
mmdc -i diagram.mmd -o diagram.png -w 1200 -b transparent
|
||||
mmdc -i diagram.mmd -o diagram.svg
|
||||
mmdc -i diagram.mmd -o diagram.pdf
|
||||
|
||||
# Specify theme configuration
|
||||
mmdc -i diagram.mmd -o diagram.png --configFile mermaid-config.json
|
||||
```
|
||||
|
||||
`mermaid-config.json` example:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "base",
|
||||
"themeVariables": {
|
||||
"primaryColor": "#EFF6FF",
|
||||
"primaryBorderColor": "#3B82F6",
|
||||
"primaryTextColor": "#1E293B",
|
||||
"lineColor": "#94A3B8",
|
||||
"secondaryColor": "#F0FDF4",
|
||||
"tertiaryColor": "#FFF7ED"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method 3: Online Preview
|
||||
|
||||
Paste code at [mermaid.live](https://mermaid.live) for instant preview and export.
|
||||
|
||||
---
|
||||
|
||||
## Theme Configuration (Design System Integration)
|
||||
|
||||
Mermaid uses `theme: 'base'` + `themeVariables` for fully custom colors.
|
||||
The following themes align with the charts skill mood color system:
|
||||
|
||||
### Business Professional (Default)
|
||||
|
||||
```javascript
|
||||
themeVariables: {
|
||||
primaryColor: '#EFF6FF', // Node background (very light blue)
|
||||
primaryBorderColor: '#3B82F6', // Node border (blue)
|
||||
primaryTextColor: '#1E293B', // Node text (dark gray-blue)
|
||||
lineColor: '#94A3B8', // Connectors (gray)
|
||||
secondaryColor: '#F0FDF4', // Secondary nodes (very light green)
|
||||
secondaryBorderColor: '#10B981',
|
||||
secondaryTextColor: '#1E293B',
|
||||
tertiaryColor: '#FFF7ED', // Tertiary nodes (very light amber)
|
||||
tertiaryBorderColor: '#F59E0B',
|
||||
tertiaryTextColor: '#1E293B',
|
||||
noteBkgColor: '#F8FAFC', // Note background
|
||||
noteTextColor: '#6B7280',
|
||||
noteBorderColor: '#E2E8F0',
|
||||
fontSize: '14px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, PingFang SC, SimHei, sans-serif',
|
||||
}
|
||||
```
|
||||
|
||||
### Tech Dark
|
||||
|
||||
```javascript
|
||||
themeVariables: {
|
||||
primaryColor: '#1E293B',
|
||||
primaryBorderColor: '#3B82F6',
|
||||
primaryTextColor: '#F1F5F9',
|
||||
lineColor: '#475569',
|
||||
secondaryColor: '#0F2E1F',
|
||||
secondaryBorderColor: '#10B981',
|
||||
secondaryTextColor: '#F1F5F9',
|
||||
tertiaryColor: '#1A1625',
|
||||
tertiaryBorderColor: '#8B5CF6',
|
||||
tertiaryTextColor: '#F1F5F9',
|
||||
noteBkgColor: '#0F172A',
|
||||
noteTextColor: '#94A3B8',
|
||||
noteBorderColor: '#334155',
|
||||
fontSize: '14px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, PingFang SC, SimHei, sans-serif',
|
||||
background: '#0F172A',
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 1: Flowchart
|
||||
|
||||
The most common chart type. Supports directions: `TB` (top→bottom), `LR` (left→right), `BT`, `RL`.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A[开始] --> B{条件判断}
|
||||
B -->|是| C[执行操作A]
|
||||
B -->|否| D[执行操作B]
|
||||
C --> E[结束]
|
||||
D --> E
|
||||
|
||||
style A fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B
|
||||
style B fill:#FFF7ED,stroke:#F59E0B,stroke-width:2px,color:#1E293B
|
||||
style C fill:#F0FDF4,stroke:#10B981,stroke-width:2px,color:#1E293B
|
||||
style D fill:#F0FDF4,stroke:#10B981,stroke-width:2px,color:#1E293B
|
||||
style E fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B
|
||||
```
|
||||
|
||||
### Node Shape Quick Reference
|
||||
|
||||
| Syntax | Shape | Use For |
|
||||
|------|------|--------|
|
||||
| `A[text]` | Rectangle | Steps/Actions |
|
||||
| `A(text)` | Rounded rect | General nodes |
|
||||
| `A([text])` | Stadium | Start/End |
|
||||
| `A{text}` | Diamond | Decision |
|
||||
| `A{{text}}` | Hexagon | Preparation |
|
||||
| `A[/text/]` | Parallelogram | Input/Output |
|
||||
| `A((text))` | Circle | Connector |
|
||||
| `A>text]` | Flag | Event/Signal |
|
||||
|
||||
### Subgraphs (Grouping)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph 前端["🖥️ 前端"]
|
||||
A[React App] --> B[API 调用]
|
||||
end
|
||||
subgraph 后端["⚙️ 后端"]
|
||||
C[FastAPI] --> D[(PostgreSQL)]
|
||||
end
|
||||
B --> C
|
||||
|
||||
style 前端 fill:#EFF6FF,stroke:#3B82F6,stroke-width:1px
|
||||
style 后端 fill:#F0FDF4,stroke:#10B981,stroke-width:1px
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 2: Sequence Diagram
|
||||
|
||||
Shows interaction sequence between systems/actors.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor 用户
|
||||
participant 前端 as 🖥️ 前端
|
||||
participant API as ⚙️ API 网关
|
||||
participant DB as 🗄️ 数据库
|
||||
|
||||
用户->>前端: 点击登录
|
||||
前端->>API: POST /auth/login
|
||||
API->>DB: 查询用户
|
||||
DB-->>API: 用户信息
|
||||
|
||||
alt 验证成功
|
||||
API-->>前端: 200 + JWT Token
|
||||
前端-->>用户: 跳转首页
|
||||
else 验证失败
|
||||
API-->>前端: 401 未授权
|
||||
前端-->>用户: 显示错误提示
|
||||
end
|
||||
|
||||
Note over 前端,API: Token 有效期 24 小时
|
||||
```
|
||||
|
||||
### Arrow Types
|
||||
|
||||
| Syntax | Meaning |
|
||||
|------|------|
|
||||
| `->>` | Solid arrow (synchronous call) |
|
||||
| `-->>` | Dashed arrow (return/response) |
|
||||
| `-x` | Solid with x (failure/rejection) |
|
||||
| `-)` | Async message |
|
||||
|
||||
---
|
||||
|
||||
## Template 3: Architecture Diagram (C4 Style via Subgraphs)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph 用户层["👤 用户层"]
|
||||
U1[Web 浏览器]
|
||||
U2[移动 App]
|
||||
end
|
||||
|
||||
subgraph 接入层["🌐 接入层"]
|
||||
GW[API Gateway<br><small>Nginx + Rate Limit</small>]
|
||||
LB[负载均衡<br><small>Round Robin</small>]
|
||||
end
|
||||
|
||||
subgraph 服务层["⚙️ 微服务"]
|
||||
S1[用户服务<br><small>FastAPI</small>]
|
||||
S2[内容服务<br><small>FastAPI</small>]
|
||||
S3[推荐服务<br><small>PyTorch</small>]
|
||||
end
|
||||
|
||||
subgraph 数据层["🗄️ 数据层"]
|
||||
DB[(PostgreSQL)]
|
||||
RD[(Redis Cache)]
|
||||
ES[(Elasticsearch)]
|
||||
end
|
||||
|
||||
U1 & U2 --> GW
|
||||
GW --> LB
|
||||
LB --> S1 & S2 & S3
|
||||
S1 --> DB & RD
|
||||
S2 --> DB & ES
|
||||
S3 --> RD & ES
|
||||
|
||||
style 用户层 fill:#EFF6FF,stroke:#3B82F6,stroke-width:1.5px
|
||||
style 接入层 fill:#FFF7ED,stroke:#F59E0B,stroke-width:1.5px
|
||||
style 服务层 fill:#F0FDF4,stroke:#10B981,stroke-width:1.5px
|
||||
style 数据层 fill:#F5F3FF,stroke:#8B5CF6,stroke-width:1.5px
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 4: Gantt Chart
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
title 项目里程碑计划
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %m/%d
|
||||
|
||||
section 需求阶段
|
||||
需求调研 :done, req1, 2024-01-01, 14d
|
||||
需求评审 :done, req2, after req1, 3d
|
||||
|
||||
section 开发阶段
|
||||
后端开发 :active, dev1, after req2, 21d
|
||||
前端开发 :active, dev2, after req2, 18d
|
||||
联调测试 : dev3, after dev1, 7d
|
||||
|
||||
section 上线阶段
|
||||
灰度发布 : rel1, after dev3, 3d
|
||||
全量上线 :milestone, rel2, after rel1, 0d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 5: Class Diagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class User {
|
||||
+String name
|
||||
+String email
|
||||
+login()
|
||||
+logout()
|
||||
}
|
||||
class Order {
|
||||
+int id
|
||||
+Date created_at
|
||||
+float total
|
||||
+submit()
|
||||
+cancel()
|
||||
}
|
||||
class Product {
|
||||
+String name
|
||||
+float price
|
||||
+int stock
|
||||
}
|
||||
|
||||
User "1" --> "*" Order : 下单
|
||||
Order "*" --> "*" Product : 包含
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 6: ER Diagram
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER {
|
||||
int id PK
|
||||
string name
|
||||
string email UK
|
||||
datetime created_at
|
||||
}
|
||||
ORDER {
|
||||
int id PK
|
||||
int user_id FK
|
||||
float total
|
||||
string status
|
||||
datetime created_at
|
||||
}
|
||||
ORDER_ITEM {
|
||||
int id PK
|
||||
int order_id FK
|
||||
int product_id FK
|
||||
int quantity
|
||||
float price
|
||||
}
|
||||
PRODUCT {
|
||||
int id PK
|
||||
string name
|
||||
float price
|
||||
int stock
|
||||
}
|
||||
|
||||
USER ||--o{ ORDER : "下单"
|
||||
ORDER ||--|{ ORDER_ITEM : "包含"
|
||||
PRODUCT ||--o{ ORDER_ITEM : "被购买"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 7: Mind Map
|
||||
|
||||
> ⚠️ Mermaid mindmap has limited layout capabilities. **For high-quality mind maps**, prefer `references/mindmap-css.md`.
|
||||
> The following approach is for **quick drafts** or embedding in Markdown documents, with CSS injection to optimize visual quality.
|
||||
|
||||
### Optimized HTML Shell (Important! Use this, not the default template)
|
||||
|
||||
Mermaid mindmap doesn't support `style`/`classDef`, but you can greatly improve results with **CSS overriding SVG styles** + **themeVariables**:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #FFFFFF;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
}
|
||||
#diagram { min-width: 900px; }
|
||||
|
||||
/* ─── CSS injection to optimize Mermaid mindmap rendering ─── */
|
||||
/*
|
||||
* Actual SVG class names in Mermaid v11 mindmap:
|
||||
* - .section-root = root node
|
||||
* - .section-0 ~ .section-N = first-level branches (in order)
|
||||
* - .section-edge-0 ~ .section-edge-N = connectors for corresponding branches
|
||||
* - .node-bkg = node background path
|
||||
* - .node-line- = node bottom decoration line
|
||||
* - .nodeLabel = text label
|
||||
* - .edge = connector
|
||||
* - .edge-depth-1/5 = connector depth level
|
||||
*/
|
||||
|
||||
/* 1. All connectors: rounded, soft */
|
||||
.edge { stroke-width: 2px !important; stroke-linecap: round !important; }
|
||||
|
||||
/* 2. Remove node bottom decoration line (ugly by default) */
|
||||
.node-line- { stroke: transparent !important; }
|
||||
|
||||
/* 3. Root node: deep blue circle + shadow */
|
||||
.section-root circle,
|
||||
.section-root ellipse {
|
||||
fill: #1E40AF !important;
|
||||
stroke: #1E3A8A !important;
|
||||
stroke-width: 3px !important;
|
||||
filter: drop-shadow(0 4px 12px rgba(30,64,175,0.35));
|
||||
}
|
||||
.section-root .nodeLabel { color: #FFFFFF !important; font-size: 17px !important; font-weight: 700 !important; }
|
||||
|
||||
/* 4. First-level branches colored in order (supports up to 8-color cycle) */
|
||||
/* Blue */
|
||||
.section-0 .node-bkg { fill: #DBEAFE !important; stroke: #3B82F6 !important; stroke-width: 2px !important; }
|
||||
.section-0 .nodeLabel { color: #1E40AF !important; font-weight: 600 !important; font-size: 14px !important; }
|
||||
.section-edge-0 { stroke: #93C5FD !important; }
|
||||
/* Green */
|
||||
.section-1 .node-bkg { fill: #D1FAE5 !important; stroke: #10B981 !important; stroke-width: 2px !important; }
|
||||
.section-1 .nodeLabel { color: #065F46 !important; font-weight: 600 !important; font-size: 14px !important; }
|
||||
.section-edge-1 { stroke: #6EE7B7 !important; }
|
||||
/* Amber */
|
||||
.section-2 .node-bkg { fill: #FEF3C7 !important; stroke: #F59E0B !important; stroke-width: 2px !important; }
|
||||
.section-2 .nodeLabel { color: #92400E !important; font-weight: 600 !important; font-size: 14px !important; }
|
||||
.section-edge-2 { stroke: #FCD34D !important; }
|
||||
/* Purple */
|
||||
.section-3 .node-bkg { fill: #EDE9FE !important; stroke: #8B5CF6 !important; stroke-width: 2px !important; }
|
||||
.section-3 .nodeLabel { color: #5B21B6 !important; font-weight: 600 !important; font-size: 14px !important; }
|
||||
.section-edge-3 { stroke: #C4B5FD !important; }
|
||||
/* Red */
|
||||
.section-4 .node-bkg { fill: #FEE2E2 !important; stroke: #EF4444 !important; stroke-width: 2px !important; }
|
||||
.section-4 .nodeLabel { color: #991B1B !important; font-weight: 600 !important; font-size: 14px !important; }
|
||||
.section-edge-4 { stroke: #FCA5A5 !important; }
|
||||
/* Cyan */
|
||||
.section-5 .node-bkg { fill: #CFFAFE !important; stroke: #06B6D4 !important; stroke-width: 2px !important; }
|
||||
.section-5 .nodeLabel { color: #155E75 !important; font-weight: 600 !important; font-size: 14px !important; }
|
||||
.section-edge-5 { stroke: #67E8F9 !important; }
|
||||
/* Pink */
|
||||
.section-6 .node-bkg { fill: #FCE7F3 !important; stroke: #EC4899 !important; stroke-width: 2px !important; }
|
||||
.section-6 .nodeLabel { color: #9D174D !important; font-weight: 600 !important; font-size: 14px !important; }
|
||||
.section-edge-6 { stroke: #F9A8D4 !important; }
|
||||
/* Gray-green */
|
||||
.section-7 .node-bkg { fill: #D1FAE5 !important; stroke: #059669 !important; stroke-width: 2px !important; }
|
||||
.section-7 .nodeLabel { color: #064E3B !important; font-weight: 600 !important; font-size: 14px !important; }
|
||||
.section-edge-7 { stroke: #6EE7B7 !important; }
|
||||
|
||||
/* 5. Deeper connectors are lighter */
|
||||
.edge-depth-5 { stroke-width: 1.5px !important; opacity: 0.6; }
|
||||
|
||||
/* 6. Light gray background */
|
||||
body { background: #FAFBFE; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="diagram">
|
||||
<pre class="mermaid">
|
||||
mindmap
|
||||
root((你的主题))
|
||||
一级分支1
|
||||
二级内容A
|
||||
二级内容B
|
||||
一级分支2
|
||||
二级内容C
|
||||
</pre>
|
||||
</div>
|
||||
<script>
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'base',
|
||||
themeVariables: {
|
||||
primaryColor: '#EFF6FF',
|
||||
primaryBorderColor: '#3B82F6',
|
||||
primaryTextColor: '#1E293B',
|
||||
lineColor: '#CBD5E1',
|
||||
fontSize: '13px',
|
||||
fontFamily: '-apple-system, BlinkMacSystemFont, PingFang SC, SimHei, sans-serif',
|
||||
},
|
||||
mindmap: {
|
||||
padding: 20,
|
||||
useMaxWidth: false,
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### Auto-Upgrade Rules (Important!)
|
||||
|
||||
> ⚠️ **Never trim user content just to fit Mermaid!**
|
||||
> Content comes first; tools serve content, not the other way around.
|
||||
|
||||
When content complexity exceeds Mermaid mindmap's comfort zone, **auto-switch to CSS approach** (`references/mindmap-css.md`):
|
||||
|
||||
| Trigger Condition (any one met) | Action |
|
||||
|----------------------|------|
|
||||
| More than 7 L1 branches | → Switch to CSS |
|
||||
| Any branch has >8 child nodes | → Switch to CSS |
|
||||
| Nesting exceeds 3 levels | → Switch to CSS |
|
||||
| Single node text >15 chars | → Switch to CSS |
|
||||
| Total nodes >40 | → Switch to CSS |
|
||||
|
||||
**When none of the above triggers**, Mermaid mindmap is adequate. The following suggestions help optimize rendering (recommendations, not hard limits):
|
||||
|
||||
| Suggestion | Notes |
|
||||
|------|------|
|
||||
| Keep node text concise | Use spaces to segment long text, avoid punctuation |
|
||||
| Use emoji prefixes per branch | Higher visual distinctiveness |
|
||||
| Use CSS injection for coloring | Use the optimized HTML shell above |
|
||||
|
||||
### Example (Optimized)
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((AI 内容运营))
|
||||
选题策划
|
||||
热点扫描
|
||||
竞品分析
|
||||
用户调研
|
||||
内容生产
|
||||
长文
|
||||
短文
|
||||
视频
|
||||
渠道分发
|
||||
微信生态
|
||||
小红书
|
||||
B站
|
||||
数据运营
|
||||
数据分析
|
||||
评论互动
|
||||
持续优化
|
||||
```
|
||||
|
||||
### Known Limitations of Mermaid Mindmap
|
||||
|
||||
- ❌ No `style` / `classDef` support for direct node coloring (CSS injection of SVG styles only)
|
||||
- ❌ Line thickness/curvature cannot be controlled from Mermaid syntax (CSS override `.mindmap-edge`)
|
||||
- ❌ Node spacing calculated by algorithm, cannot be manually specified
|
||||
- ❌ Long CJK text easily overlaps with connectors (strict character count control needed)
|
||||
- ⚠️ CSS injection depends on Mermaid internal class naming, may break on version upgrades
|
||||
|
||||
**Conclusion**: For quick drafts use Mermaid + the CSS-optimized shell above; for production output use CSS mind map → `references/mindmap-css.md`
|
||||
|
||||
---
|
||||
|
||||
## Template 8: State Diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 草稿
|
||||
草稿 --> 审核中 : 提交审核
|
||||
审核中 --> 已发布 : 审核通过
|
||||
审核中 --> 草稿 : 退回修改
|
||||
已发布 --> 已下架 : 违规/过期
|
||||
已下架 --> 草稿 : 重新编辑
|
||||
已发布 --> [*] : 永久删除
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 9: Git Branch Graph
|
||||
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit id: "init"
|
||||
branch develop
|
||||
checkout develop
|
||||
commit id: "feat: 用户模块"
|
||||
commit id: "feat: 订单模块"
|
||||
branch feature/payment
|
||||
checkout feature/payment
|
||||
commit id: "feat: 支付接入"
|
||||
commit id: "fix: 金额精度"
|
||||
checkout develop
|
||||
merge feature/payment id: "merge: 支付"
|
||||
checkout main
|
||||
merge develop id: "release: v1.0"
|
||||
commit id: "hotfix: 安全补丁" type: REVERSE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling Tips
|
||||
|
||||
### Single Node Style
|
||||
|
||||
```mermaid
|
||||
style 节点ID fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B
|
||||
```
|
||||
|
||||
### Batch Styles (classDef)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
classDef blue fill:#EFF6FF,stroke:#3B82F6,stroke-width:1.5px,color:#1E293B
|
||||
classDef green fill:#F0FDF4,stroke:#10B981,stroke-width:1.5px,color:#1E293B
|
||||
classDef amber fill:#FFF7ED,stroke:#F59E0B,stroke-width:1.5px,color:#1E293B
|
||||
|
||||
A[步骤1]:::blue --> B{判断}:::amber
|
||||
B -->|是| C[结果A]:::green
|
||||
B -->|否| D[结果B]:::green
|
||||
```
|
||||
|
||||
### Connector Styles
|
||||
|
||||
```mermaid
|
||||
%% Style for the N-th connector (0-indexed)
|
||||
linkStyle 0 stroke:#3B82F6,stroke-width:2px
|
||||
linkStyle 1 stroke:#10B981,stroke-width:2px,stroke-dasharray: 5 5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mermaid vs Other Approaches
|
||||
|
||||
| Capability | Mermaid | Playwright+CSS | draw.io |
|
||||
|------|---------|---------------|---------|
|
||||
| Learning curve | ✅ Very low (Markdown-like) | Medium (HTML/CSS) | ✅ Very low (drag&drop) |
|
||||
| Version control friendly | ✅ Plain text | ✅ Plain text | ❌ XML binary |
|
||||
| Flowcharts | ✅ Built-in | ⚠️ Manual layout | ✅ Drag&drop |
|
||||
| Sequence diagrams | ✅ Built-in | ❌ Very complex | ✅ Templates |
|
||||
| Gantt charts | ✅ Built-in | ❌ Build from scratch | ⚠️ Limited |
|
||||
| Class/ER diagrams | ✅ Built-in | ❌ Not suited | ✅ Templates |
|
||||
| Visual freedom | ⚠️ Limited | ✅ Full freedom | ✅ Free |
|
||||
| PNG export | ✅ mmdc/Playwright | ✅ Playwright | ✅ Built-in |
|
||||
| CJK support | ✅ Native | ✅ Font config | ✅ Native |
|
||||
| Auto layout | ✅ Automatic | ❌ Manual | ⚠️ Semi-auto |
|
||||
|
||||
**Principle: Use Mermaid for structural/relationship diagrams, Playwright+CSS for creative design diagrams.**
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: CJK node names cause layout issues?
|
||||
|
||||
Ensure `fontFamily` includes a CJK font:
|
||||
```javascript
|
||||
themeVariables: {
|
||||
fontFamily: '-apple-system, PingFang SC, SimHei, sans-serif'
|
||||
}
|
||||
```
|
||||
|
||||
### Q: How to control node spacing?
|
||||
|
||||
Mermaid auto-layouts; spacing is adjusted via config:
|
||||
```javascript
|
||||
flowchart: { padding: 16, nodeSpacing: 50, rankSpacing: 60 }
|
||||
```
|
||||
|
||||
### Q: Chart too large?
|
||||
|
||||
- Split into subgraphs
|
||||
- Change direction (`TB` too tall → switch to `LR`)
|
||||
- Use `mmdc -w 1600` to increase canvas width
|
||||
|
||||
### Q: How to add line breaks in nodes?
|
||||
|
||||
Use `<br>` tags:
|
||||
```mermaid
|
||||
A[第一行<br>第二行<br><small>小字注释</small>]
|
||||
```
|
||||
|
||||
### Q: Flowchart node text truncated or overlapping?
|
||||
|
||||
**Common causes and fixes**:
|
||||
|
||||
1. **Insufficient node padding**: ensure `flowchart.padding` is at least `24` (CJK chars are ~50% wider than Latin)
|
||||
2. **Text too long**: use `<br>` for manual line breaks, or shorten text
|
||||
3. **Canvas too narrow**: use `width: fit-content` on `#diagram` container (🚫 NEVER use `max-width` — Mermaid SVG width is unpredictable)
|
||||
4. **Node spacing too small**: increase `nodeSpacing` and `rankSpacing` (recommended 60+)
|
||||
5. **When using classDef**: ensure `font-size` isn't too large, 12-14px is ideal
|
||||
|
||||
**Correct approach for long-text nodes**:
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["这是一段比较长的<br>需要换行的文字"]
|
||||
B["用引号包裹节点文字<br>可以使用 HTML 标签"]
|
||||
```
|
||||
|
||||
**Key configuration**:
|
||||
```javascript
|
||||
mermaid.initialize({
|
||||
flowchart: {
|
||||
padding: 32,
|
||||
nodeSpacing: 80,
|
||||
rankSpacing: 80,
|
||||
htmlLabels: true,
|
||||
wrappingWidth: 160,
|
||||
}
|
||||
});
|
||||
```
|
||||
Executable
+911
@@ -0,0 +1,911 @@
|
||||
# CSS Mind Map Rendering Engine
|
||||
|
||||
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
|
||||
|
||||
**Core principle: Content-driven, not template-driven. First analyze the content structure, then decide on the layout, and finally render.**
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Content Analysis
|
||||
|
||||
After receiving a mind map requirement, **don't write any HTML/CSS yet**. First parse the content into a tree structure, then calculate key metrics.
|
||||
|
||||
### 1.1 Build Tree JSON
|
||||
|
||||
```
|
||||
Input: "How to work efficiently from home", branches include "Environment Setup", "Time Management", "Tool Selection"...
|
||||
↓
|
||||
Output:
|
||||
{
|
||||
"root": "在家如何高效办公",
|
||||
"branches": [
|
||||
{
|
||||
"label": "环境准备",
|
||||
"children": ["独立工作区", "降噪耳机", "人体工学椅"]
|
||||
},
|
||||
{
|
||||
"label": "时间管理",
|
||||
"children": [
|
||||
{ "label": "番茄工作法", "children": ["25分钟专注", "5分钟休息"] },
|
||||
"日程规划",
|
||||
"避免多任务"
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Key Metrics
|
||||
|
||||
| Metric | Calculation Method | What It Affects |
|
||||
|------|---------|---------|
|
||||
| `branchCount` | Number of first-level branches | Choose single-sided/dual-sided expansion |
|
||||
| `maxDepth` | Maximum nesting depth | Canvas width, whether sub-branch is needed |
|
||||
| `maxChildren` | Maximum children per node | Vertical height of that branch |
|
||||
| `totalNodes` | Total number of all nodes | Overall canvas size |
|
||||
| `maxTextLen` | Longest text character count | Node width, whether line breaks are needed |
|
||||
| `branchWeights[]` | Total descendants per first-level branch | Left-right balance allocation |
|
||||
|
||||
### 1.3 Example Analysis
|
||||
|
||||
```
|
||||
Content: "产品经理核心技能", 6 L1 branches, max depth 3, total 35 nodes, max text length 12 chars
|
||||
→ branchCount=6, maxDepth=3, totalNodes=35, maxTextLen=12
|
||||
→ branchWeights=[8, 5, 7, 4, 6, 5]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Layout Decision
|
||||
|
||||
Based on the metrics from Step 1, choose a layout scheme. The following are reference suggestions, not hard rules—adjust flexibly according to actual content.
|
||||
|
||||
### 2.1 Layout Selection Guide
|
||||
|
||||
**Core principle: Use simple layouts for less content, dual-sided layouts for more content, avoid one side becoming too long.**
|
||||
|
||||
```
|
||||
Few branches (roughly ≤4), simple content?
|
||||
→ Style A: Right-expanding tree (single-sided, compact)
|
||||
|
||||
Many branches (roughly ≥5), or single side would be too long?
|
||||
→ Style B: Left-right expanding tree (dual-sided balanced, most common)
|
||||
|
||||
User explicitly requests a specific form?
|
||||
→ "cards"/"modules" → Style C: Card grid
|
||||
→ "fishbone"/"root cause analysis" → Style D: Fishbone diagram
|
||||
```
|
||||
|
||||
**Why not use radial layout?** Radial layout (spreading from center outward) almost inevitably causes node overlap when there are more than 3-4 branches, and is extremely difficult to debug. Not recommended.
|
||||
|
||||
### 2.2 Canvas Size Calculation
|
||||
|
||||
Don't use fixed sizes. Calculate based on content:
|
||||
|
||||
```python
|
||||
def calc_canvas(branch_count, max_depth, total_nodes, max_text_len, layout):
|
||||
# Width: depends on depth and text length
|
||||
node_width = max(120, max_text_len * 14) # ~14px per CJK char (including padding)
|
||||
if layout == 'left-right':
|
||||
width = node_width * max_depth * 2 + 200 # Dual-sided, 200px in center for root node
|
||||
else:
|
||||
width = node_width * max_depth + 200 # Single-sided
|
||||
|
||||
# Height: depends on max branch vertical expansion
|
||||
if layout == 'left-right':
|
||||
max_side_nodes = total_nodes // 2 + 2 # rough estimate of single-side nodes
|
||||
else:
|
||||
max_side_nodes = total_nodes
|
||||
height = max_side_nodes * 32 + 200 # ~32px per leaf (including gap)
|
||||
|
||||
# Lower bounds
|
||||
width = max(width, 1200)
|
||||
height = max(height, 600)
|
||||
|
||||
return width, height
|
||||
```
|
||||
|
||||
### 2.3 Left-Right Branch Allocation (Style B)
|
||||
|
||||
Goal: Achieve similar visual weight on both sides.
|
||||
|
||||
```python
|
||||
def balance_branches(branch_weights):
|
||||
"""Greedy bin-packing: sort by weight descending, alternate left/right"""
|
||||
indexed = sorted(enumerate(branch_weights), key=lambda x: -x[1])
|
||||
left, right = [], []
|
||||
left_sum, right_sum = 0, 0
|
||||
for idx, weight in indexed:
|
||||
if left_sum <= right_sum:
|
||||
left.append(idx)
|
||||
left_sum += weight
|
||||
else:
|
||||
right.append(idx)
|
||||
right_sum += weight
|
||||
return left, right
|
||||
# e.g.: weights=[8,5,7,4,6,5] → left=[0,3,5] right=[2,4,1] → 17 vs 18
|
||||
```
|
||||
|
||||
### 2.4 Node Styling Decision
|
||||
|
||||
**Principle: The deeper the level, the lighter the visual weight.** This way readers can distinguish main branches from details at a glance.
|
||||
|
||||
```
|
||||
Root node → most prominent: dark solid background, large font (~20px)
|
||||
L1 branches → next prominent: light fill + colored border, medium font (~15px)
|
||||
L2 nodes → lighter still: paler fill + thin border, medium-small font (~13px)
|
||||
Leaf nodes → lightest: capsule frame or plain text, small font (~12-13px)
|
||||
```
|
||||
|
||||
**Key: Leaves should not have the same visual weight as first-level branches.** Leaves' padding, gap, and border thickness should all be significantly smaller than their parent. Otherwise the chart will be vertically too long and lack hierarchy.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Rendering
|
||||
|
||||
Based on the decisions from Step 2, generate HTML + CSS + JS.
|
||||
|
||||
### 3.1 Playwright Screenshot (Universal)
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
async def mindmap_to_png(html_path, png_path, width=1600):
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page(viewport={'width': width, 'height': 1200}, device_scale_factor=2)
|
||||
await page.goto(f'file://{html_path}', wait_until='networkidle')
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
el = page.locator('#mindmap')
|
||||
bbox = await el.bounding_box()
|
||||
# First expansion: ensure content is not clipped
|
||||
expand_w = max(width, int(bbox['width'] + 100))
|
||||
expand_h = int(bbox['height'] + 100)
|
||||
await page.set_viewport_size({'width': expand_w, 'height': expand_h})
|
||||
await page.wait_for_timeout(200)
|
||||
|
||||
# Call connector script
|
||||
await page.evaluate('if(typeof drawAllLines==="function") drawAllLines()')
|
||||
await page.wait_for_timeout(200)
|
||||
|
||||
# Second contraction: measure actual content right edge, trim right-side blank space
|
||||
trim = await page.evaluate('''() => {
|
||||
const map = document.getElementById('mindmap');
|
||||
const nodes = map.querySelectorAll('.root-node,.branch-node,.sub-node,.leaf,.deep-node');
|
||||
const mapRect = map.getBoundingClientRect();
|
||||
let maxR = 0, maxB = 0;
|
||||
nodes.forEach(n => {
|
||||
const r = n.getBoundingClientRect();
|
||||
maxR = Math.max(maxR, r.right - mapRect.left);
|
||||
maxB = Math.max(maxB, r.bottom - mapRect.top);
|
||||
});
|
||||
return { contentW: Math.ceil(maxR) + 80, contentH: Math.ceil(maxB) + 80 };
|
||||
}''')
|
||||
await page.set_viewport_size({'width': trim['contentW'], 'height': trim['contentH']})
|
||||
await page.wait_for_timeout(200)
|
||||
# Redraw connectors (viewport changed so coordinates change)
|
||||
await page.evaluate('if(typeof drawAllLines==="function") drawAllLines()')
|
||||
await page.wait_for_timeout(200)
|
||||
|
||||
await el.screenshot(path=png_path)
|
||||
await browser.close()
|
||||
|
||||
import os
|
||||
print(f'✅ {png_path} ({os.path.getsize(png_path)/1024:.0f}KB)')
|
||||
```
|
||||
|
||||
### 3.2 Universal Recursive Connector Script v4-fix (All Styles)
|
||||
|
||||
This script automatically handles tree connectors of **any depth** (3, 4, 5 levels... all work). HTML for styles A and B should include this script at the end.
|
||||
|
||||
**⚠️ DOM Structure Convention (connector script depends on this structure):**
|
||||
|
||||
```
|
||||
#mindmap
|
||||
.tree-layout ← flex container (for left-right tree)
|
||||
.left-side ← left branch area
|
||||
.branch.c-{color} ← L1 branch (must have color class)
|
||||
.branch-node ← L1 node
|
||||
.children ← L2 container
|
||||
div ← child wrapper
|
||||
.sub-node ← L2 node
|
||||
.leaf-group ← L3 container
|
||||
div / .leaf ← leaf or wrapper with deeper levels
|
||||
.leaf ← L3 leaf
|
||||
.deep-group ← L4 container (recursive, same as above)
|
||||
.deep-node ← L4/L5 node
|
||||
.center-root
|
||||
.root-node ← root node
|
||||
.right-side ← right branches (same structure as .left-side)
|
||||
```
|
||||
|
||||
**Also supports legacy structure (.branches/.right-branches/.left-branches), backward compatible.**
|
||||
|
||||
**⚠️ CSS Indentation Rules (connectors depend on child nodes having offset relative to parent, no indentation = broken connectors):**
|
||||
```css
|
||||
/* .tree-layout structure (new version) must include these paddings */
|
||||
.left-side .children, .left-side .leaf-group, .left-side .deep-group {
|
||||
align-items: flex-end; padding-right: 16px;
|
||||
}
|
||||
.right-side .children, .right-side .leaf-group, .right-side .deep-group {
|
||||
align-items: flex-start; padding-left: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
**Common causes of missing connectors/layout errors:**
|
||||
- **⚠️ `.sub-branch` missing `display: flex`** (most critical! Without flex, `flex-direction: row-reverse` doesn't work, left-side leaves won't expand left, instead all pile up on the right)
|
||||
- **⚠️ Child node containers missing padding-left/padding-right** (without indentation, child nodes align with parent, midX is outside child nodes, connectors break)
|
||||
- **⚠️ `.lr-tree` / `.tree` should not have `z-index`** (creates stacking context, covers SVG connectors)
|
||||
- **⚠️ Leaf nodes must NOT stretch to equal width** — each leaf should size to its own text content (`white-space: nowrap` or `width: fit-content`). Never add `width: 100%`, `flex-grow: 1`, or `align-items: stretch` to leaf containers. Leaves with shorter text should be narrower than leaves with longer text.
|
||||
- Leaf container not named `.leaf-group` (using `.child-list`, `.sub-items`, etc.)
|
||||
- Deep-level container not named `.deep-group`
|
||||
- `#mindmap` missing `position: relative`
|
||||
|
||||
**Key improvements (v4-fix vs v2):**
|
||||
- **No transparency**—use solid color blend for fading (`color + '80'` is almost invisible on white background)
|
||||
- **Recursively process `.children`, `.leaf-group`, `.deep-group`**—no longer limited to 3 levels
|
||||
- **Vertical line draws complete range**—one line from `min(Y)` to `max(Y)`, instead of drawing per child node
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Universal recursive connector script v4-fix
|
||||
* - Supports any nesting depth (recursively processes .children + .leaf-group + .deep-group)
|
||||
* - Unified gray-tone connectors (#64748B → #94A3B8 → #A8B4C2), visually clean
|
||||
* - Direction logic:
|
||||
* dir='left': startX=parent.left → midX(left-biased) → endX=child.right
|
||||
* dir='right': startX=parent.right → midX(right-biased) → endX=child.left
|
||||
*/
|
||||
function drawAllLines() {
|
||||
const map = document.getElementById('mindmap');
|
||||
if (!map) { console.error('❌ #mindmap not found'); return; }
|
||||
const cRect = map.getBoundingClientRect();
|
||||
|
||||
const old = map.querySelector('svg.lines');
|
||||
if (old) old.remove();
|
||||
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.classList.add('lines');
|
||||
svg.setAttribute('width', cRect.width);
|
||||
svg.setAttribute('height', cRect.height);
|
||||
svg.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:1;';
|
||||
let lineCount = 0;
|
||||
|
||||
function rel(el) {
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
cx: r.left - cRect.left + r.width/2,
|
||||
cy: r.top - cRect.top + r.height/2,
|
||||
left: r.left - cRect.left,
|
||||
right: r.right - cRect.left,
|
||||
};
|
||||
}
|
||||
|
||||
function drawLine(x1, y1, x2, y2, color, width) {
|
||||
const l = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
// Round to nearest pixel to prevent sub-pixel misalignment (visual kinks)
|
||||
l.setAttribute('x1', Math.round(x1)); l.setAttribute('y1', Math.round(y1));
|
||||
l.setAttribute('x2', Math.round(x2)); l.setAttribute('y2', Math.round(y2));
|
||||
l.setAttribute('stroke', color); l.setAttribute('stroke-width', width);
|
||||
l.setAttribute('stroke-linecap', 'round');
|
||||
svg.appendChild(l);
|
||||
lineCount++;
|
||||
}
|
||||
|
||||
// ─── Unified gray connectors (decreasing by depth, visually clean) ───
|
||||
const lineStyles = [
|
||||
{ color: '#64748B', width: 2.5 }, // root → L1
|
||||
{ color: '#94A3B8', width: 2 }, // L1 → L2
|
||||
{ color: '#A8B4C2', width: 1.5 }, // L2 → L3
|
||||
{ color: '#B8C2CC', width: 1.2 }, // L3 → L4
|
||||
{ color: '#CBD5E1', width: 1 }, // L4 → L5
|
||||
];
|
||||
function getLineStyle(branchColor, depth) {
|
||||
const s = lineStyles[Math.min(depth, lineStyles.length - 1)];
|
||||
return { color: s.color, width: s.width };
|
||||
}
|
||||
|
||||
// ─── Connector direction ───
|
||||
// dir='left': parent.left → midX → child.right (each child gets its own polyline)
|
||||
// dir='right': parent.right → midX → child.left
|
||||
//
|
||||
// [Principle] midX is the X coordinate of the vertical line; it must be in the gap between parent and children.
|
||||
// Use a fraction of the parent-to-nearest-child distance as offset, so:
|
||||
// - Large node spacing → large offset, lines spread out
|
||||
// - Small node spacing → small offset, lines compact without crossing text
|
||||
// All children in the same connect() call share one midX, ensuring vertical line alignment.
|
||||
function connect(parentEl, childEls, color, width, dir) {
|
||||
if (!childEls.length) return;
|
||||
const p = rel(parentEl);
|
||||
const startX = dir === 'left' ? p.left : p.right;
|
||||
const startY = p.cy;
|
||||
|
||||
// Special case: single child — draw one straight horizontal line, no vertical spine
|
||||
if (childEls.length === 1) {
|
||||
const c = rel(childEls[0]);
|
||||
const endX = dir === 'left' ? c.right : c.left;
|
||||
const midY = Math.round((startY + c.cy) / 2);
|
||||
drawLine(startX, midY, endX, midY, color, width);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the closest child edge to calculate available space
|
||||
let closestEdge;
|
||||
if (dir === 'left') {
|
||||
closestEdge = Math.max(...childEls.map(ch => rel(ch).right));
|
||||
} else {
|
||||
closestEdge = Math.min(...childEls.map(ch => rel(ch).left));
|
||||
}
|
||||
// Place midX at the midpoint between parent edge and closest child edge,
|
||||
// but guarantee at least 16px clearance from child nodes
|
||||
const childClearance = 16;
|
||||
const midpoint = startX + (closestEdge - startX) / 2;
|
||||
const midFromChild = dir === 'left' ? closestEdge + childClearance : closestEdge - childClearance;
|
||||
// Use the position that's further from children (safer)
|
||||
const midX = dir === 'left'
|
||||
? Math.min(midpoint, midFromChild)
|
||||
: Math.max(midpoint, midFromChild);
|
||||
|
||||
drawLine(startX, startY, midX, startY, color, width);
|
||||
|
||||
// Draw ONE continuous vertical line spanning from parent to the last child
|
||||
const allCYs = childEls.map(ch => rel(ch).cy);
|
||||
const minY = Math.min(startY, ...allCYs);
|
||||
const maxY = Math.max(startY, ...allCYs);
|
||||
drawLine(midX, minY, midX, maxY, color, width);
|
||||
|
||||
// Then draw horizontal lines from the vertical spine to each child
|
||||
childEls.forEach(ch => {
|
||||
const c = rel(ch);
|
||||
const endX = dir === 'left' ? c.right : c.left;
|
||||
const endY = c.cy;
|
||||
drawLine(midX, endY, endX, endY, color, width);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Recursively process subtree (supports .children + .leaf-group + .deep-group) ───
|
||||
const NODE_SEL = '.branch-node, .sub-node, .leaf, .deep-node';
|
||||
const CONTAINER_SEL = ':scope > .children, :scope > .leaf-group, :scope > .deep-group';
|
||||
|
||||
function processChildren(parentNodeEl, containerEl, branchColor, depth, dir) {
|
||||
if (!containerEl) return;
|
||||
const childNodeEls = [];
|
||||
for (const wrapper of containerEl.children) {
|
||||
const nodeEl = wrapper.matches?.(NODE_SEL) ? wrapper : wrapper.querySelector(NODE_SEL);
|
||||
if (nodeEl) childNodeEls.push(nodeEl);
|
||||
}
|
||||
if (!childNodeEls.length) return;
|
||||
const style = getLineStyle(branchColor, depth);
|
||||
connect(parentNodeEl, childNodeEls, style.color, style.width, dir);
|
||||
|
||||
for (const wrapper of containerEl.children) {
|
||||
const nodeEl = wrapper.matches?.(NODE_SEL) ? wrapper : wrapper.querySelector(NODE_SEL);
|
||||
if (!nodeEl) continue;
|
||||
wrapper.querySelectorAll(CONTAINER_SEL).forEach(nc =>
|
||||
processChildren(nodeEl, nc, branchColor, depth + 1, dir)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Main flow ───
|
||||
const rootNode = map.querySelector('.root-node');
|
||||
if (!rootNode) { console.error('❌ .root-node not found'); return; }
|
||||
const rp = rel(rootNode);
|
||||
|
||||
// Left side: collect all L1 branch-nodes, draw polylines with connect() (with vertical spine)
|
||||
const leftSel = '.left-side > .branch, .left-branches > .left-branch, .left-branches > div';
|
||||
const leftBranches = map.querySelectorAll(leftSel);
|
||||
const leftBranchNodes = [];
|
||||
leftBranches.forEach(branch => {
|
||||
const bNode = branch.querySelector('.branch-node');
|
||||
if (bNode) leftBranchNodes.push(bNode);
|
||||
});
|
||||
if (leftBranchNodes.length) {
|
||||
connect(rootNode, leftBranchNodes, lineStyles[0].color, lineStyles[0].width, 'left');
|
||||
}
|
||||
leftBranches.forEach(branch => {
|
||||
const bNode = branch.querySelector('.branch-node');
|
||||
if (!bNode) return;
|
||||
processChildren(bNode, branch.querySelector(':scope > .children'), null, 1, 'left');
|
||||
});
|
||||
|
||||
// Right side: same as above
|
||||
const rightSel = '.right-side > .branch, .branches > .branch, .right-branches > .right-branch';
|
||||
const rightBranches = map.querySelectorAll(rightSel);
|
||||
const rightBranchNodes = [];
|
||||
rightBranches.forEach(branch => {
|
||||
const bNode = branch.querySelector('.branch-node');
|
||||
if (bNode) rightBranchNodes.push(bNode);
|
||||
});
|
||||
if (rightBranchNodes.length) {
|
||||
connect(rootNode, rightBranchNodes, lineStyles[0].color, lineStyles[0].width, 'right');
|
||||
}
|
||||
rightBranches.forEach(branch => {
|
||||
const bNode = branch.querySelector('.branch-node');
|
||||
if (!bNode) return;
|
||||
processChildren(bNode, branch.querySelector(':scope > .children'), null, 1, 'right');
|
||||
});
|
||||
|
||||
map.insertBefore(svg, map.firstChild);
|
||||
console.log(`✅ Drew ${lineCount} lines`);
|
||||
}
|
||||
```
|
||||
|
||||
**How to call (at end of HTML):**
|
||||
```html
|
||||
<script>
|
||||
/* ← paste the drawAllLines function above */
|
||||
window.addEventListener('load', () => setTimeout(drawAllLines, 300));
|
||||
</script>
|
||||
```
|
||||
|
||||
### 3.3 Universal CSS Base (Shared by All Styles)
|
||||
|
||||
```css
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #FFFFFF;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
|
||||
}
|
||||
#mindmap { padding: 60px; display: inline-block; min-width: 100%; position: relative; }
|
||||
#mindmap > svg.lines {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
pointer-events: none; z-index: 0;
|
||||
}
|
||||
|
||||
/* ─── Root Node ─── */
|
||||
/* ─── Topic Intent Color System ─── */
|
||||
/* Model picks ONE intent based on content semantics. Add data-intent="xxx" to #mindmap.
|
||||
DO NOT manually write hex colors for root node — use intent system only. */
|
||||
|
||||
/* Intent → Root Node + Branch Palette mapping */
|
||||
#mindmap[data-intent="professional"] .root-node { background: linear-gradient(135deg, #1E3A5F, #2D4A6F); box-shadow: 0 4px 12px rgba(30,58,95,0.25); }
|
||||
#mindmap[data-intent="technical"] .root-node { background: linear-gradient(135deg, #334155, #475569); box-shadow: 0 4px 12px rgba(51,65,85,0.25); }
|
||||
#mindmap[data-intent="medical"] .root-node { background: linear-gradient(135deg, #0F766E, #0D9488); box-shadow: 0 4px 12px rgba(15,118,110,0.25); }
|
||||
#mindmap[data-intent="education"] .root-node { background: linear-gradient(135deg, #9A3412, #B45309); box-shadow: 0 4px 12px rgba(154,52,18,0.25); }
|
||||
#mindmap[data-intent="creative"] .root-node { background: linear-gradient(135deg, #7C3AED, #8B5CF6); box-shadow: 0 4px 12px rgba(124,58,237,0.25); }
|
||||
#mindmap[data-intent="finance"] .root-node { background: linear-gradient(135deg, #1E3A5F, #1E40AF); box-shadow: 0 4px 12px rgba(30,58,95,0.25); }
|
||||
#mindmap[data-intent="nature"] .root-node { background: linear-gradient(135deg, #166534, #15803D); box-shadow: 0 4px 12px rgba(22,101,52,0.25); }
|
||||
#mindmap[data-intent="warning"] .root-node { background: linear-gradient(135deg, #991B1B, #B91C1C); box-shadow: 0 4px 12px rgba(153,27,27,0.25); }
|
||||
#mindmap[data-intent="neutral"] .root-node { background: linear-gradient(135deg, #334155, #475569); box-shadow: 0 4px 12px rgba(51,65,85,0.25); }
|
||||
|
||||
/*
|
||||
Intent Selection Guide (for model):
|
||||
professional → corporate reports, strategy, management, business plans
|
||||
technical → software, engineering, architecture, systems, AI/ML
|
||||
medical → healthcare, clinical, pharmaceutical, nursing, anatomy
|
||||
education → teaching, learning, curriculum, training, academic
|
||||
creative → design, art, marketing, branding, media
|
||||
finance → banking, investment, accounting, economics, trading
|
||||
nature → environment, ecology, agriculture, biology, geography
|
||||
warning → risk analysis, safety, incident review, compliance, audit
|
||||
neutral → general topics, mixed content, unclear domain
|
||||
|
||||
Recommended branch color combos per intent:
|
||||
professional → [blue, teal, cyan]
|
||||
technical → [blue, purple, cyan]
|
||||
medical → [teal, green, blue]
|
||||
education → [amber, green, blue]
|
||||
creative → [purple, amber, cyan]
|
||||
finance → [blue, green, cyan]
|
||||
nature → [green, teal, amber]
|
||||
warning → [red, amber, cyan]
|
||||
neutral → [blue, green, purple]
|
||||
*/
|
||||
|
||||
.root-node {
|
||||
color: white; font-size: 20px; font-weight: 700;
|
||||
padding: 18px 28px; border-radius: 12px;
|
||||
white-space: nowrap; flex-shrink: 0; align-self: center;
|
||||
}
|
||||
/* Fallback if no intent specified — defaults to professional blue */
|
||||
#mindmap:not([data-intent]) .root-node {
|
||||
background: linear-gradient(135deg, #1E3A5F, #2D4A6F);
|
||||
box-shadow: 0 4px 12px rgba(30,58,95,0.25);
|
||||
}
|
||||
|
||||
/* ─── Container Layout ─── */
|
||||
.tree { display: flex; align-items: flex-start; gap: 0; position: relative; }
|
||||
.branches { display: flex; flex-direction: column; gap: 16px; margin-left: 60px; }
|
||||
.branch, .sub-branch { display: flex; align-items: flex-start; gap: 0; }
|
||||
|
||||
/* ─── First-Level Branch Node ─── */
|
||||
.branch-node {
|
||||
font-size: 15px; font-weight: 600;
|
||||
padding: 10px 20px; border-radius: 8px;
|
||||
white-space: nowrap; flex-shrink: 0; border: 2px solid;
|
||||
}
|
||||
/* ─── Second-Level Sub-Node (when has children) ─── */
|
||||
.sub-node {
|
||||
font-size: 13px; font-weight: 500;
|
||||
padding: 7px 14px; border-radius: 6px;
|
||||
white-space: nowrap; flex-shrink: 0; border: 1.5px solid;
|
||||
background: #F8FAFC;
|
||||
}
|
||||
/* ─── Leaf Node ─── */
|
||||
/* Default: lightweight capsule frame */
|
||||
.leaf {
|
||||
font-size: 13px; font-weight: 400; color: #475569;
|
||||
padding: 4px 10px; background: #FAFAFA;
|
||||
border-radius: 12px; border: 1px solid #E5E7EB;
|
||||
white-space: nowrap; line-height: 1.5;
|
||||
}
|
||||
/* Text-only mode (more compact, add class="leaf text-only") */
|
||||
.leaf.text-only {
|
||||
background: none; border: none; padding: 2px 0; border-radius: 0;
|
||||
}
|
||||
/* Leaf supplementary description */
|
||||
.leaf-desc {
|
||||
font-size: 12px; color: #94A3B8; font-weight: 400; margin-left: 8px;
|
||||
}
|
||||
.leaf-desc::before { content: '— '; color: #CBD5E1; }
|
||||
|
||||
/* ─── Child Node Container ─── */
|
||||
/* Principle: The deeper the level, the smaller the gap, but not so small that connectors and text overlap */
|
||||
.children { display: flex; flex-direction: column; gap: 6px; margin-left: 48px; align-items: flex-start; }
|
||||
.children:has(.sub-branch) { gap: 10px; } /* sub-branch is larger than leaf, needs more spacing */
|
||||
.sub-branch .children { gap: 5px; margin-left: 40px; align-items: flex-start; }
|
||||
.sub-branch .sub-branch .children { gap: 4px; margin-left: 36px; align-items: flex-start; }
|
||||
/* Tip: If leaf text and connectors overlap, prioritize increasing gap */
|
||||
|
||||
/* ─── Color System (for distinguishing different branches) ─── */
|
||||
.c-blue { background: #EFF6FF; border-color: #3B82F6; color: #1E40AF; }
|
||||
.c-green { background: #F0FDF4; border-color: #10B981; color: #065F46; }
|
||||
.c-amber { background: #FFF7ED; border-color: #F59E0B; color: #92400E; }
|
||||
.c-purple { background: #F5F3FF; border-color: #8B5CF6; color: #5B21B6; }
|
||||
.c-red { background: #FEF2F2; border-color: #EF4444; color: #991B1B; }
|
||||
.c-cyan { background: #ECFEFF; border-color: #06B6D4; color: #155E75; }
|
||||
.c-teal { background: #F0FDFA; border-color: #14B8A6; color: #134E4A; }
|
||||
|
||||
/* Second-level sub-node inherits branch color scheme (lighter) */
|
||||
.c-blue .sub-node { background: #F0F7FF; border-color: #93C5FD; color: #1E40AF; }
|
||||
.c-green .sub-node { background: #F2FDF6; border-color: #6EE7B7; color: #065F46; }
|
||||
.c-amber .sub-node { background: #FFF9F0; border-color: #FCD34D; color: #92400E; }
|
||||
.c-purple .sub-node { background: #F8F6FF; border-color: #C4B5FD; color: #5B21B6; }
|
||||
.c-red .sub-node { background: #FFF5F5; border-color: #FCA5A5; color: #991B1B; }
|
||||
.c-cyan .sub-node { background: #F0FEFF; border-color: #67E8F9; color: #155E75; }
|
||||
.c-teal .sub-node { background: #F2FDFA; border-color: #5EEAD4; color: #134E4A; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style A: Right-Expanding Tree (Simple scenarios with branchCount ≤ 4 or maxDepth ≤ 2)
|
||||
|
||||
### HTML Structure
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
/* ← paste the "Universal CSS Base" above */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mindmap" data-intent="professional">
|
||||
<div class="tree">
|
||||
<div class="root-node">中心主题</div>
|
||||
<div class="branches">
|
||||
|
||||
<div class="branch">
|
||||
<div class="branch-node c-blue">一级分支A</div>
|
||||
<div class="children">
|
||||
<div class="leaf">叶子1</div>
|
||||
<div class="leaf">叶子2</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="branch">
|
||||
<div class="branch-node c-green">一级分支B</div>
|
||||
<div class="children">
|
||||
<div class="sub-branch">
|
||||
<div class="sub-node c-green">二级有下级</div>
|
||||
<div class="children">
|
||||
<div class="leaf">三级叶子1</div>
|
||||
<div class="leaf">三级叶子2</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="leaf">二级叶子</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
/* ← paste the "Universal Recursive Connector Script" */
|
||||
drawAllLines();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style B: Left-Right Expanding Tree (Standard scenario with branchCount ≥ 5, most common)
|
||||
|
||||
### Additional CSS (append after universal base)
|
||||
|
||||
```css
|
||||
/* ─── Left-Right Expanding Layout ─── */
|
||||
.lr-tree { display: flex; align-items: center; gap: 0; position: relative; }
|
||||
.lr-tree .root-node {
|
||||
padding: 20px 32px; border-radius: 14px;
|
||||
margin: 0 60px; text-align: center;
|
||||
}
|
||||
|
||||
/* ⚠️ sub-branch must be flex, otherwise flex-direction: row-reverse won't work, leaves won't expand left */
|
||||
.sub-branch { display: flex; align-items: flex-start; gap: 0; }
|
||||
|
||||
.left-branches { display: flex; flex-direction: column; gap: 16px; }
|
||||
.left-branch { display: flex; align-items: flex-start; flex-direction: row-reverse; gap: 0; }
|
||||
.left-branch .children {
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
margin-right: 48px; align-items: flex-end;
|
||||
}
|
||||
.left-branch .children:has(.sub-branch) { gap: 10px; }
|
||||
.left-branch .sub-branch .children { gap: 5px; margin-right: 40px; margin-left: 0; align-items: flex-end; }
|
||||
.left-branch .sub-branch { flex-direction: row-reverse; }
|
||||
|
||||
.right-branches { display: flex; flex-direction: column; gap: 16px; }
|
||||
.right-branch { display: flex; align-items: flex-start; gap: 0; }
|
||||
.right-branch .children {
|
||||
display: flex; flex-direction: column; gap: 6px; margin-left: 48px; align-items: flex-start;
|
||||
}
|
||||
.right-branch .children:has(.sub-branch) { gap: 10px; }
|
||||
.right-branch .sub-branch .children { gap: 5px; margin-left: 40px; align-items: flex-start; }
|
||||
```
|
||||
|
||||
### HTML Structure
|
||||
|
||||
```html
|
||||
<div id="mindmap" data-intent="professional">
|
||||
<div class="lr-tree">
|
||||
|
||||
<!-- Left branches (allocated by balance_branches) -->
|
||||
<div class="left-branches">
|
||||
<div class="left-branch">
|
||||
<div class="branch-node c-purple">分支D(放左边)</div>
|
||||
<div class="children">
|
||||
<div class="leaf">叶子1</div>
|
||||
<div class="leaf">叶子2</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- More left branches... -->
|
||||
</div>
|
||||
|
||||
<div class="root-node">中心主题</div>
|
||||
|
||||
<!-- Right branches -->
|
||||
<div class="right-branches">
|
||||
<div class="right-branch">
|
||||
<div class="branch-node c-blue">分支A(放右边)</div>
|
||||
<div class="children">
|
||||
<div class="leaf">叶子1</div>
|
||||
<div class="leaf">叶子2</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- More right branches... -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
/* ← paste the "Universal Recursive Connector Script" */
|
||||
drawAllLines(); // v4: auto-handles both left and right sides
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style C: Card Grid (when user explicitly requests "cards" / "modules")
|
||||
|
||||
> ⚠️ This is not a mind map — it's a modular display. No connectors; use cards + color coding to show relationships.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #FFFFFF;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
|
||||
}
|
||||
#mindmap { padding: 48px 64px; }
|
||||
.map-title {
|
||||
font-size: 24px; font-weight: 700; color: #1E293B; text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.map-subtitle {
|
||||
font-size: 14px; color: #64748B; text-align: center;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 20px; max-width: 1000px; margin: 0 auto;
|
||||
}
|
||||
.card {
|
||||
background: #FFFFFF; border-radius: 12px;
|
||||
padding: 20px; border: 1px solid #E2E8F0;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
||||
}
|
||||
.card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; padding-bottom: 10px; border-bottom: 2px solid; }
|
||||
.card-title { font-size: 15px; font-weight: 600; }
|
||||
.card-icon {
|
||||
width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; border-radius: 50%; flex-shrink: 0;
|
||||
}
|
||||
.card-items { list-style: none; display: flex; flex-direction: column; gap: 6px; }
|
||||
.card-items li {
|
||||
font-size: 13px; color: #475569; padding-left: 16px; position: relative;
|
||||
}
|
||||
.card-items li::before {
|
||||
content: ''; position: absolute; left: 0; top: 8px;
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Color variants */
|
||||
.card.blue .card-header { border-color: #3B82F6; }
|
||||
.card.blue .card-icon { background: #EFF6FF; }
|
||||
.card.blue .card-title { color: #1E40AF; }
|
||||
.card.blue li::before { background: #3B82F6; }
|
||||
|
||||
.card.green .card-header { border-color: #10B981; }
|
||||
.card.green .card-icon { background: #F0FDF4; }
|
||||
.card.green .card-title { color: #065F46; }
|
||||
.card.green li::before { background: #10B981; }
|
||||
|
||||
.card.amber .card-header { border-color: #F59E0B; }
|
||||
.card.amber .card-icon { background: #FFF7ED; }
|
||||
.card.amber .card-title { color: #92400E; }
|
||||
.card.amber li::before { background: #F59E0B; }
|
||||
|
||||
.card.purple .card-header { border-color: #8B5CF6; }
|
||||
.card.purple .card-icon { background: #F5F3FF; }
|
||||
.card.purple .card-title { color: #5B21B6; }
|
||||
.card.purple li::before { background: #8B5CF6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mindmap" data-intent="professional">
|
||||
<div class="map-title">标题</div>
|
||||
<div class="map-subtitle">副标题</div>
|
||||
<div class="card-grid">
|
||||
<div class="card blue">
|
||||
<div class="card-header">
|
||||
<div class="card-icon">📋</div>
|
||||
<div class="card-title">模块A</div>
|
||||
</div>
|
||||
<ul class="card-items">
|
||||
<li>条目1</li>
|
||||
<li>条目2</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- More cards... -->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Style D: Fishbone Diagram (Problem Analysis / Root Cause)
|
||||
|
||||
Best for problem analysis, root cause tracing, quality management (Ishikawa diagram).
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #FFFFFF;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
|
||||
}
|
||||
#mindmap { padding: 48px 64px; }
|
||||
.fishbone { position: relative; }
|
||||
.spine { display: flex; align-items: center; gap: 0; }
|
||||
.spine-line { flex: 1; height: 3px; background: #1E293B; }
|
||||
.spine-head {
|
||||
width: 0; height: 0;
|
||||
border-left: 16px solid #1E293B;
|
||||
border-top: 10px solid transparent;
|
||||
border-bottom: 10px solid transparent;
|
||||
}
|
||||
.result-node {
|
||||
background: #1E293B; color: white;
|
||||
font-size: 16px; font-weight: 700;
|
||||
padding: 12px 24px; border-radius: 8px;
|
||||
white-space: nowrap; margin-left: 4px;
|
||||
}
|
||||
.bone-branches {
|
||||
position: absolute; left: 80px; right: 200px; top: 50%;
|
||||
display: flex; justify-content: space-around;
|
||||
}
|
||||
.bone { display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.bone.up { transform: translateY(calc(-100% - 20px)); }
|
||||
.bone.down { transform: translateY(20px); }
|
||||
.bone-title {
|
||||
font-size: 14px; font-weight: 600;
|
||||
padding: 8px 16px; border-radius: 8px; white-space: nowrap;
|
||||
}
|
||||
.bone-items { display: flex; flex-direction: column; gap: 4px; align-items: center; }
|
||||
.bone-item {
|
||||
font-size: 12px; color: #64748B;
|
||||
padding: 3px 10px; background: #F8FAFC;
|
||||
border-radius: 4px; border: 1px solid #E2E8F0; white-space: nowrap;
|
||||
}
|
||||
.bone-line { width: 2px; height: 24px; background: #CBD5E1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="mindmap" data-intent="professional">
|
||||
<div class="fishbone">
|
||||
<div class="spine">
|
||||
<div class="spine-line"></div>
|
||||
<div class="spine-head"></div>
|
||||
<div class="result-node">问题/结果</div>
|
||||
</div>
|
||||
<!-- Upper causes -->
|
||||
<div class="bone-branches" style="transform: translateY(calc(-50% - 20px));">
|
||||
<div class="bone">
|
||||
<div class="bone-items">
|
||||
<div class="bone-item">子原因1</div>
|
||||
<div class="bone-item">子原因2</div>
|
||||
</div>
|
||||
<div class="bone-line"></div>
|
||||
<div class="bone-title" style="background:#EFF6FF;color:#1E40AF;border:1.5px solid #3B82F6;">原因类别A</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Lower causes -->
|
||||
<div class="bone-branches" style="transform: translateY(calc(-50% + 20px));">
|
||||
<div class="bone">
|
||||
<div class="bone-title" style="background:#F0FDF4;color:#065F46;border:1.5px solid #10B981;">原因类别B</div>
|
||||
<div class="bone-line"></div>
|
||||
<div class="bone-items">
|
||||
<div class="bone-item">子原因1</div>
|
||||
<div class="bone-item">子原因2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
After rendering, verify against this checklist:
|
||||
|
||||
1. **Content complete** — Every node from the original requirement is in the map, nothing missing
|
||||
2. **Clear hierarchy** — L1 branches and leaves are instantly distinguishable (different bg/border/font-weight)
|
||||
3. **No overlap** — No boxes covering boxes, no lines through text
|
||||
4. **Connectors visible** — Every parent-child pair has a connector, no orphan leaves. Connector color ≥ `#94A3B8` (too light = invisible)
|
||||
5. **Connector direction correct** — Left-side leaves extend left, right-side extends right
|
||||
6. **Proportions reasonable** — Map is not extremely narrow/tall (target aspect ratio 1:1 to 3:1), visually comfortable
|
||||
7. **Text readable** — At final output size, smallest text is legible. Reference: root 18px+, L1 15px+, L2 13px+, leaves 12px+
|
||||
8. **Roughly balanced** (Style B) — Visual weight approximately equal on both sides, doesn't need to be symmetric. Target ≤30% difference
|
||||
9. **Canvas large enough** — No nodes clipped, sufficient padding on all sides (reference 60px+)
|
||||
10. **No large blank areas on right** — Screenshot trimmed to content edge
|
||||
|
||||
---
|
||||
|
||||
## ⛔ Radial Layout Warning
|
||||
|
||||
**Radial layout is strongly discouraged.** Using `position: absolute` for fixed branch positions leads to overlap when branches increase, and deep levels cannot be handled.
|
||||
|
||||
If content is very simple (reference: ≤3 branches, ≤3 children per branch, text ≤6 chars, depth ≤2, total ≤12 nodes), it's technically possible, but tree layout is always the safer choice.
|
||||
|
||||
**No code template provided.** If conditions are met, manually adjust based on Style A.
|
||||
Executable
+801
@@ -0,0 +1,801 @@
|
||||
# Playwright + CSS Rendering Engine
|
||||
|
||||
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
|
||||
|
||||
**Core principle: Content-driven, not template-driven. Analyze content structure first, then decide layout, then render.**
|
||||
|
||||
Applicable to: flowcharts, infographics, KPI cards, data posters — any visualization requiring full CSS power (gradients, shadows, rounded corners, Grid/Flexbox layout).
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Content Analysis
|
||||
|
||||
When you receive a flowchart/infographic requirement, **don't write HTML/CSS first**. Analyze the content structure.
|
||||
|
||||
### 1.1 Flowchart Content Analysis
|
||||
|
||||
```
|
||||
Input: "Honey production process", 8 steps, linear without branches
|
||||
↓
|
||||
{
|
||||
"type": "flowchart",
|
||||
"nodes": [
|
||||
{ "id": "1", "label": "花粉采集", "desc": "蜜蜂从花朵采集花蜜" },
|
||||
{ "id": "2", "label": "酿造", "desc": "蜜蜂在蜂巢中反复吞吐" },
|
||||
...
|
||||
],
|
||||
"edges": [["1","2"], ["2","3"], ...],
|
||||
"nodeTypes": { "start": ["1"], "end": ["8"], "decision": [], "normal": ["2","3","4","5","6","7"] }
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 Key Metrics
|
||||
|
||||
| Metric | Calculation | Affects |
|
||||
|------|---------|---------|
|
||||
| `nodeCount` | Total node count | Mermaid vs CSS flowchart |
|
||||
| `maxTextLen` | Longest text char count | Node width |
|
||||
| `hasDecision` | Has decision branches | Layout complexity |
|
||||
| `hasBranch` | Has parallel/branches | Column count |
|
||||
| `parallelCount` | Max parallel branches | Column count |
|
||||
| `phaseCount` | Phase/group count | Need for phased containers |
|
||||
| `hasRoles` | Has roles/swimlanes | Need for dual-panel |
|
||||
| `isLinear` | Linear without branches | Can use snake layout |
|
||||
|
||||
### 1.3 Infographic Content Analysis
|
||||
|
||||
Infographics (KPI cards, data posters) are simpler to analyze:
|
||||
- How many data metrics? → Determines grid columns
|
||||
- Any trend data? → Need for sparklines
|
||||
- Title area? → Need for hero header
|
||||
- Comparison data? → Need for bar chart
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Layout Decision
|
||||
|
||||
### 2.1 Flowchart Layout Decision Tree
|
||||
|
||||
```
|
||||
⚠️ DEFAULT RULE: When user asks "generate/create XXX 流程图" without specifying format,
|
||||
DEFAULT to Layout C (Phased Vertical). Almost all real-world processes have phases.
|
||||
|
||||
User specified Mermaid/markdown?
|
||||
└─ Yes → Follow user choice (Format Constraint Rule)
|
||||
└─ No →
|
||||
nodeCount ≤ 6 AND no phases AND maxTextLen ≤ 8 (CJK)?
|
||||
└─ Yes → Mermaid (minimal flowchart)
|
||||
└─ No →
|
||||
phaseCount > 0 OR nodeCount ≥ 5?
|
||||
└─ Yes → ⭐ Layout C: CSS Phased Vertical flowchart (DEFAULT)
|
||||
└─ No →
|
||||
hasRoles?
|
||||
└─ Yes → Layout D: CSS Dual-panel/Swimlane flowchart
|
||||
└─ No → Layout C: CSS Phased Vertical flowchart (fallback also uses C)
|
||||
```
|
||||
|
||||
**⚠️ Layout A (Grid) and Layout B (Snake) are only for very special scenarios (e.g., flat comparison, unordered parallel items). Flowcharts default to Layout C.**
|
||||
|
||||
### 🚫 Flowchart Anti-Patterns (FORBIDDEN)
|
||||
|
||||
| ❌ Bad Pattern | ✅ Correct Pattern |
|
||||
|---|---|
|
||||
| Phase titles (一、二、三...) as isolated left-side text labels | Phase titles as colored title bars, wrapped inside group cards |
|
||||
| All nodes flat-laid in Grid without group containers | Each phase wrapped in a `.phase-group` card containing its steps |
|
||||
| Role labels scattered above nodes | Role info displayed uniformly at the top of the flowchart, or as phase card labels |
|
||||
| Nodes connected with loose diverging lines | Phases connected with arrows (↓), steps within phases use numbering |
|
||||
| Inconsistent node sizes, uneven spacing, misaligned | Same-phase nodes share uniform style, overall alignment consistent |
|
||||
| Using Layout A Grid for a phased flowchart | Has phases → must use Layout C |
|
||||
|
||||
### 2.2 Canvas Size Calculation
|
||||
|
||||
```python
|
||||
def calc_flowchart_canvas(node_count, max_text_len, parallel_count, has_roles, layout):
|
||||
node_width = max(180, max_text_len * 16) # ~16px per CJK char (including padding)
|
||||
|
||||
if layout == 'snake':
|
||||
cols = min(4, node_count)
|
||||
rows = (node_count + cols - 1) // cols
|
||||
width = cols * (node_width + 60) + 120 # 60=gap, 120=padding
|
||||
height = rows * 120 + 200 # 120=row height
|
||||
elif layout == 'dual-panel':
|
||||
width = max(1600, node_width * 2 + 400) # left panel + right flow
|
||||
height = node_count * 100 + 200
|
||||
elif layout == 'phased-vertical':
|
||||
width = max(800, node_width + 200)
|
||||
height = node_count * 80 + 200
|
||||
else: # grid
|
||||
cols = max(parallel_count, 2)
|
||||
width = cols * (node_width + 60) + 120
|
||||
rows = (node_count + cols - 1) // cols
|
||||
height = rows * 120 + 200
|
||||
|
||||
return max(width, 800), max(height, 600)
|
||||
```
|
||||
|
||||
### 2.3 Color Decision
|
||||
|
||||
**Iron rule: Node background = low-saturation light color, border = high-saturation color. Large high-saturation areas = children's drawing.**
|
||||
|
||||
**⚠️ Text contrast iron rule: Dark/accent background nodes must use light text (white or near-white) for title and description.**
|
||||
Light background → dark text (`#1F2937`), dark background → light text (`#FFFFFF` or `#FFF7ED`).
|
||||
Common mistake: Endpoint/highlight node switched to dark background, but description text remains dark gray, making it completely unreadable.
|
||||
|
||||
```
|
||||
Node type → color:
|
||||
Start/end → bg: #EFF6FF, border: #3B82F6 (blue), text: #1E40AF
|
||||
Normal step → bg: #F8FAFC, border: #94A3B8 (gray-blue), text: #374151 or colored by phase
|
||||
Decision node → bg: #FFF7ED, border: #F59E0B (amber), text: #92400E
|
||||
Success/pass → bg: #F0FDF4, border: #10B981 (green), text: #065F46
|
||||
End/failure → bg: #F5F3FF, border: #8B5CF6 (purple), text: #5B21B6
|
||||
Emphasis/endpoint (dark bg) → bg: #92400E, border: #F59E0B, text: #FFFFFF, desc: #FFF7ED
|
||||
|
||||
Max 3-4 background colors for nodes in the same flowchart.
|
||||
Overall chart background = white #FFFFFF.
|
||||
```
|
||||
|
||||
**Phase bar colors**: Same-hue gradient (blue-gray family), **never use different hues per phase**.
|
||||
|
||||
```css
|
||||
/* ✅ Same-hue blue-gray progression */
|
||||
.phase-1 { background: #F0F4F8; border-left: 4px solid #64748B; }
|
||||
.phase-2 { background: #E8EDF2; border-left: 4px solid #5B7A99; }
|
||||
|
||||
/* ❌ Different hue per phase (rainbow effect) */
|
||||
.phase-1 { background: #EFF6FF; border-left: 4px solid #3B82F6; } /* blue */
|
||||
.phase-2 { background: #F0FDF4; border-left: 4px solid #10B981; } /* green */
|
||||
.phase-3 { background: #FFF7ED; border-left: 4px solid #F59E0B; } /* amber */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Rendering
|
||||
|
||||
### 3.1 Playwright Screenshot (Universal)
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
async def html_to_image(html_path, output_path, selector='#root',
|
||||
width=1200, height=None, scale=2):
|
||||
"""HTML → PNG/PDF
|
||||
|
||||
scale: 2 (default crisp), 1.5 (large canvas 3000px+), 3 (print).
|
||||
Width must accommodate ALL content. After first render, auto-resize viewport to fit.
|
||||
"""
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
page = await browser.new_page(
|
||||
viewport={'width': width, 'height': height or 800},
|
||||
device_scale_factor=scale
|
||||
)
|
||||
await page.goto(f'file://{html_path}', wait_until='networkidle')
|
||||
await page.wait_for_timeout(500)
|
||||
|
||||
if output_path.endswith('.pdf'):
|
||||
await page.pdf(path=output_path, print_background=True)
|
||||
else:
|
||||
el = page.locator(selector)
|
||||
bbox = await el.bounding_box()
|
||||
if bbox:
|
||||
fit_w = max(width, int(bbox['width'] + 100))
|
||||
fit_h = int(bbox['height'] + 100)
|
||||
await page.set_viewport_size({'width': fit_w, 'height': fit_h})
|
||||
await page.wait_for_timeout(200)
|
||||
await el.screenshot(path=output_path)
|
||||
|
||||
await browser.close()
|
||||
import os
|
||||
print(f'✅ {output_path} ({os.path.getsize(output_path)/1024:.0f}KB)')
|
||||
```
|
||||
|
||||
### 3.2 HTML Universal Shell
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
:root {
|
||||
--text: #111827;
|
||||
--text-sub: #6B7280;
|
||||
--text-muted: #9CA3AF;
|
||||
--bg: #FFFFFF;
|
||||
--surface: #F9FAFB;
|
||||
--border: #E5E7EB;
|
||||
--blue: #3B82F6;
|
||||
--cyan: #06B6D4;
|
||||
--purple: #8B5CF6;
|
||||
--amber: #F59E0B;
|
||||
--red: #EF4444;
|
||||
--green: #10B981;
|
||||
--positive: #22C55E;
|
||||
--negative: #EF4444;
|
||||
--connector: #94A3B8;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', system-ui, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#root { width: fit-content; min-width: 800px; margin: 0 auto; padding: 48px 40px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<!-- Content -->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### 3.3 CSS Variables: Node Color System
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Node types — low-saturation background + high-saturation border */
|
||||
--node-bg: #EFF6FF; --node-border: #3B82F6; /* Normal step (blue) */
|
||||
--node-decision-bg: #FFF7ED; --node-decision-border: #F59E0B; /* Decision (amber) */
|
||||
--node-success-bg: #F0FDF4; --node-success-border: #10B981; /* Success (green) */
|
||||
--node-end-bg: #F5F3FF; --node-end-border: #8B5CF6; /* End (purple) */
|
||||
--group-bg: #F8FAFC; --group-border: #E2E8F0; /* Group container */
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout A: CSS Grid Flowchart (Universal, Most Common)
|
||||
|
||||
For: Flowcharts with branches/decisions, >10 nodes or long CJK text.
|
||||
|
||||
### Core CSS
|
||||
|
||||
```css
|
||||
.flow-title {
|
||||
font-size: 22px; font-weight: 700; color: var(--text);
|
||||
text-align: center; margin-bottom: 40px;
|
||||
}
|
||||
.flow-subtitle {
|
||||
font-size: 14px; color: var(--text-sub);
|
||||
text-align: center; margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Grid container */
|
||||
.flow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 220px));
|
||||
gap: 40px 60px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Node */
|
||||
.flow-node {
|
||||
background: var(--node-bg); border: 2px solid var(--node-border);
|
||||
border-radius: 10px; padding: 16px 20px;
|
||||
text-align: center; position: relative; z-index: 1;
|
||||
min-height: 56px; display: flex; flex-direction: column;
|
||||
justify-content: center; align-items: center;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
max-width: 260px; /* Prevent single node from being too wide and crowding parallel nodes */
|
||||
word-break: break-word; /* Force line break for overly long text */
|
||||
white-space: normal; /* Allow line breaks (override possible nowrap) */
|
||||
}
|
||||
.flow-node .node-title { font-size: 15px; font-weight: 600; line-height: 1.4; }
|
||||
.flow-node .node-desc { font-size: 12px; color: var(--text-sub); margin-top: 4px; }
|
||||
|
||||
/* Node variants */
|
||||
.flow-node.start { border-radius: 24px; }
|
||||
.flow-node.decision { background: var(--node-decision-bg); border-color: var(--node-decision-border); }
|
||||
.flow-node.success { background: var(--node-success-bg); border-color: var(--node-success-border); }
|
||||
.flow-node.end { background: var(--node-end-bg); border-color: var(--node-end-border); }
|
||||
|
||||
/* Group box */
|
||||
.flow-group {
|
||||
background: var(--group-bg); border: 1.5px dashed var(--group-border);
|
||||
border-radius: 12px; padding: 24px 20px 20px; position: relative;
|
||||
}
|
||||
.flow-group .group-label {
|
||||
position: absolute; top: -10px; left: 16px;
|
||||
background: var(--bg); padding: 0 8px;
|
||||
font-size: 12px; font-weight: 600; color: var(--text-sub);
|
||||
}
|
||||
|
||||
/* ─── Parallel branch constraints (prevent overlap when multiple nodes in same row) ─── */
|
||||
/*
|
||||
⚠️ Parallel branch iron rules:
|
||||
1. Gap between parallel nodes in same row ≥ 40px (guaranteed by .flow-grid gap)
|
||||
2. Each node max-width: 260px + word-break: break-word (set in .flow-node)
|
||||
3. If parallel nodes > 3 → switch to vertical branch layout (don't force-squeeze into one row)
|
||||
4. Text over 15 CJK characters → must line-break, don't expand node width
|
||||
5. When using flex instead of manual grid-column for parallel areas, add flex-wrap: wrap as fallback
|
||||
*/
|
||||
.parallel-group {
|
||||
display: flex; gap: 40px; justify-content: center;
|
||||
flex-wrap: wrap; /* Fallback: auto-wrap when exceeding width */
|
||||
}
|
||||
.parallel-group .flow-node {
|
||||
flex: 0 1 220px; /* Max 220px, can shrink, won't grow infinitely */
|
||||
}
|
||||
|
||||
/* SVG connector layer */
|
||||
.flow-connectors {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
pointer-events: none; z-index: 0;
|
||||
}
|
||||
.flow-connectors line, .flow-connectors path {
|
||||
stroke: var(--connector); stroke-width: 2; fill: none;
|
||||
marker-end: url(#arrowhead);
|
||||
}
|
||||
.connector-label {
|
||||
font-size: 12px; fill: var(--text-sub); text-anchor: middle;
|
||||
font-family: -apple-system, 'PingFang SC', 'SimHei', sans-serif;
|
||||
}
|
||||
|
||||
/* Legend — must be independent container, not inside flow-grid */
|
||||
.flow-legend {
|
||||
display: flex; gap: 24px; justify-content: center;
|
||||
margin-top: 40px; padding: 16px 24px;
|
||||
background: #F9FAFB; border-radius: 8px; border: 1px solid #E5E7EB;
|
||||
}
|
||||
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #4B5563; }
|
||||
.legend-dot { width: 12px; height: 12px; border-radius: 3px; border: 2px solid; }
|
||||
```
|
||||
|
||||
### Auto Connector Script
|
||||
|
||||
```javascript
|
||||
// connections = [['sourceID', 'targetID', 'label'], ...]
|
||||
function drawConnectors(connections) {
|
||||
const svg = document.getElementById('connectorSvg');
|
||||
const container = svg.parentElement;
|
||||
const cRect = container.getBoundingClientRect();
|
||||
|
||||
svg.setAttribute('width', cRect.width);
|
||||
svg.setAttribute('height', cRect.height);
|
||||
svg.setAttribute('viewBox', `0 0 ${cRect.width} ${cRect.height}`);
|
||||
|
||||
// Clear old connectors (keep defs)
|
||||
svg.querySelectorAll('line, path, text.connector-label').forEach(el => el.remove());
|
||||
|
||||
connections.forEach(([fromId, toId, label]) => {
|
||||
const fromEl = document.querySelector(`[data-id="${fromId}"]`);
|
||||
const toEl = document.querySelector(`[data-id="${toId}"]`);
|
||||
if (!fromEl || !toEl) return;
|
||||
|
||||
const f = fromEl.getBoundingClientRect();
|
||||
const t = toEl.getBoundingClientRect();
|
||||
const x1 = f.left + f.width/2 - cRect.left;
|
||||
const y1 = f.bottom - cRect.top;
|
||||
const x2 = t.left + t.width/2 - cRect.left;
|
||||
const y2 = t.top - cRect.top;
|
||||
|
||||
if (Math.abs(x1 - x2) < 10) {
|
||||
// Same column → straight line
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
|
||||
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
|
||||
svg.appendChild(line);
|
||||
} else {
|
||||
// Different column → bent line
|
||||
const midY = (y1 + y2) / 2;
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
path.setAttribute('d', `M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`);
|
||||
svg.appendChild(path);
|
||||
}
|
||||
|
||||
if (label) {
|
||||
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||
text.setAttribute('x', (x1 + x2) / 2);
|
||||
text.setAttribute('y', (y1 + y2) / 2 - 6);
|
||||
text.setAttribute('class', 'connector-label');
|
||||
text.textContent = label;
|
||||
svg.appendChild(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// SVG defs (arrow definitions)
|
||||
function ensureArrowDef(svg) {
|
||||
if (svg.querySelector('#arrowhead')) return;
|
||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||
defs.innerHTML = '<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0, 10 3.5, 0 7" fill="#94A3B8" /></marker>';
|
||||
svg.appendChild(defs);
|
||||
}
|
||||
```
|
||||
|
||||
### HTML Structure Example
|
||||
|
||||
```html
|
||||
<div id="root">
|
||||
<div class="flow-title">流程图标题</div>
|
||||
|
||||
<div style="position: relative;">
|
||||
<svg class="flow-connectors" id="connectorSvg"></svg>
|
||||
<div class="flow-grid" id="flowGrid">
|
||||
<div class="flow-node start" data-id="start" style="grid-column: 2;">
|
||||
<div class="node-title">开始</div>
|
||||
</div>
|
||||
<div class="flow-node" data-id="step1" style="grid-column: 1;">
|
||||
<div class="node-title">步骤一</div>
|
||||
<div class="node-desc">详细说明</div>
|
||||
</div>
|
||||
<div class="flow-node decision" data-id="decide" style="grid-column: 2;">
|
||||
<div class="node-title">判断条件?</div>
|
||||
</div>
|
||||
<div class="flow-node end" data-id="end" style="grid-column: 2;">
|
||||
<div class="node-title">结束</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-legend">
|
||||
<div class="legend-item"><div class="legend-dot" style="border-color:#3B82F6;background:#EFF6FF;"></div>步骤</div>
|
||||
<div class="legend-item"><div class="legend-dot" style="border-color:#F59E0B;background:#FFF7ED;"></div>判断</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const connections = [
|
||||
['start', 'step1', ''], ['step1', 'decide', ''],
|
||||
['decide', 'end', '通过']
|
||||
];
|
||||
window.addEventListener('load', () => {
|
||||
ensureArrowDef(document.getElementById('connectorSvg'));
|
||||
setTimeout(() => drawConnectors(connections), 100);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout B: Snake Flowchart (>4 Linear Steps)
|
||||
|
||||
For: Linear non-branching processes with many steps (5-20).
|
||||
|
||||
### Key Rules
|
||||
|
||||
- Max 4 nodes per row
|
||||
- First row left→right, second row right→left (snake pattern)
|
||||
- End-of-row to next-row turn connectors use bend lines, with ≥60px clearance at turns
|
||||
- Node positions in grid are auto-calculated by JS (no manual grid-column)
|
||||
|
||||
### Snake Layout Generation Script
|
||||
|
||||
```javascript
|
||||
function layoutSnake(nodeIds, cols) {
|
||||
cols = cols || 4;
|
||||
nodeIds.forEach((id, i) => {
|
||||
const row = Math.floor(i / cols);
|
||||
const colInRow = i % cols;
|
||||
const col = row % 2 === 0 ? colInRow + 1 : cols - colInRow; // Even rows L→R, odd rows R→L
|
||||
const el = document.querySelector(`[data-id="${id}"]`);
|
||||
if (el) {
|
||||
el.style.gridRow = row + 1;
|
||||
el.style.gridColumn = col;
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout C: Phased Vertical Flowchart (⭐ DEFAULT for all flowcharts)
|
||||
|
||||
**This is the DEFAULT layout for flowcharts.** When the user asks for any flowchart without specifying format, use this layout.
|
||||
|
||||
For: Any process with phases/stages, which is nearly ALL real-world processes (manufacturing, legal, project management, business operations, etc.). Also the safe fallback when unsure.
|
||||
|
||||
**Why this is the default:** Layout C produces consistently professional, readable results. Even if the process has only 2 "phases", the card-based grouping still looks clean. In contrast, Layout A (Grid) without proper grouping produces scattered, unreadable results.
|
||||
|
||||
### Key Design
|
||||
|
||||
**Phase titles vs sub-steps must have clear visual distinction**:
|
||||
- Phase titles: colored background, font-size ≥ 16px, font-weight: 700
|
||||
- Sub-steps: white/light gray background, font-size 14-15px, font-weight: 400-500
|
||||
|
||||
**No arrows between sub-steps**. Arrows only connect phase-to-phase. Sub-steps use indent + numbering for sequence.
|
||||
|
||||
### Phase Connector Direction Rule
|
||||
Phase-to-phase connector arrows MUST match the logical flow direction. If the flow goes top → bottom, arrows point ↓. If bottom → top, arrows point ↑. If left → right, arrows point →. **Never draw arrows opposing the flow direction.**
|
||||
|
||||
### Additional CSS
|
||||
|
||||
```css
|
||||
.phase-group {
|
||||
background: #F8FAFC; border-radius: 12px; padding: 20px 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.phase-title {
|
||||
font-size: 16px; font-weight: 700; padding: 10px 16px;
|
||||
border-radius: 8px; margin-bottom: 16px;
|
||||
}
|
||||
.phase-steps { display: flex; flex-direction: column; gap: 8px; padding-left: 12px; }
|
||||
.phase-step {
|
||||
font-size: 14px; font-weight: 400; color: var(--text);
|
||||
padding: 8px 14px; background: white; border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.phase-step .step-num {
|
||||
display: inline-block; width: 22px; height: 22px; line-height: 22px;
|
||||
text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Phase colors — same-hue blue-gray gradient (low saturation, easy on the eyes)
|
||||
All phases share the blue-gray color family, distinguished by brightness progression.
|
||||
🚫 FORBIDDEN: Different hue per phase (blue→green→amber→purple) — becomes rainbow with many phases.
|
||||
✅ CORRECT: Progress within same hue family (light→dark), or pure grayscale + single-color accent.
|
||||
|
||||
Two schemes provided below; model selects based on phase count:
|
||||
- ≤4 phases: Scheme A (blue-gray progression)
|
||||
- 5-7 phases: Scheme B (neutral gray base + blue accent progression)
|
||||
- >7 phases: all use same gray base, distinguish only by numbering
|
||||
*/
|
||||
|
||||
/* Scheme A: Blue-gray progression (≤4 phases) */
|
||||
.phase-1 .phase-title { background: #F0F4F8; color: #334155; border-left: 4px solid #64748B; }
|
||||
.phase-1 .step-num { background: #E2E8F0; color: #475569; }
|
||||
.phase-2 .phase-title { background: #E8EDF2; color: #1E3A5F; border-left: 4px solid #5B7A99; }
|
||||
.phase-2 .step-num { background: #DBEAFE; color: #1E3A5F; }
|
||||
.phase-3 .phase-title { background: #E0E7EF; color: #1E3050; border-left: 4px solid #4A6B8A; }
|
||||
.phase-3 .step-num { background: #D0D9E4; color: #1E3050; }
|
||||
.phase-4 .phase-title { background: #D8E0EA; color: #172540; border-left: 4px solid #3A5C7A; }
|
||||
.phase-4 .step-num { background: #C7D2E0; color: #172540; }
|
||||
|
||||
/* Scheme B: Neutral gray base + blue accent progression (5-7 phases) */
|
||||
/*
|
||||
.phase-1 .phase-title { background: #F8FAFC; color: #334155; border-left: 4px solid #94A3B8; }
|
||||
.phase-1 .step-num { background: #F1F5F9; color: #475569; }
|
||||
.phase-2 .phase-title { background: #F1F5F9; color: #334155; border-left: 4px solid #7B8FA3; }
|
||||
.phase-2 .step-num { background: #E2E8F0; color: #475569; }
|
||||
.phase-3 .phase-title { background: #E8EDF3; color: #2D4156; border-left: 4px solid #6B809A; }
|
||||
.phase-3 .step-num { background: #DBEAFE; color: #2D4156; }
|
||||
.phase-4 .phase-title { background: #E2E8F0; color: #283C52; border-left: 4px solid #5B7590; }
|
||||
.phase-4 .step-num { background: #D0D9E4; color: #283C52; }
|
||||
.phase-5 .phase-title { background: #DAE1EB; color: #23364D; border-left: 4px solid #4B6A87; }
|
||||
.phase-5 .step-num { background: #C7D2E0; color: #23364D; }
|
||||
.phase-6 .phase-title { background: #D2DAE5; color: #1E3048; border-left: 4px solid #3B5F7D; }
|
||||
.phase-6 .step-num { background: #BFC9D8; color: #1E3048; }
|
||||
.phase-7 .phase-title { background: #CAD3E0; color: #192A3E; border-left: 4px solid #2B5473; }
|
||||
.phase-7 .step-num { background: #B7C2D2; color: #192A3E; }
|
||||
*/
|
||||
```
|
||||
|
||||
### HTML Structure
|
||||
|
||||
```html
|
||||
<div id="root">
|
||||
<div class="flow-title">项目流程</div>
|
||||
|
||||
<div class="phase-group phase-1">
|
||||
<div class="phase-title">第一阶段:需求分析</div>
|
||||
<div class="phase-steps">
|
||||
<div class="phase-step"><span class="step-num">1</span>需求收集与整理</div>
|
||||
<div class="phase-step"><span class="step-num">2</span>可行性评估</div>
|
||||
<div class="phase-step"><span class="step-num">3</span>需求优先级排序</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase-to-phase arrow (use SVG or simple centered down arrow) -->
|
||||
<div style="text-align:center; color:#94A3B8; font-size:24px; margin: 8px 0;">↓</div>
|
||||
|
||||
<div class="phase-group phase-2">
|
||||
<div class="phase-title">第二阶段:设计开发</div>
|
||||
<div class="phase-steps">
|
||||
<div class="phase-step"><span class="step-num">4</span>UI/UX 设计</div>
|
||||
<div class="phase-step"><span class="step-num">5</span>前后端开发</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout D: Dual-Panel / Swimlane Flowchart
|
||||
|
||||
For: Processes involving multiple roles or departments.
|
||||
|
||||
### Key Rules
|
||||
|
||||
- Canvas width ≥ 1600px
|
||||
- Left panel for role/swimlane labels, right panel for flow nodes
|
||||
- Role labels font-size ≥ 12px, solid background, right edge ≥ 40px from canvas
|
||||
- `overflow: hidden` is forbidden
|
||||
|
||||
### Additional CSS
|
||||
|
||||
```css
|
||||
.dual-layout { display: flex; gap: 40px; }
|
||||
.role-panel {
|
||||
flex-shrink: 0; width: 160px;
|
||||
display: flex; flex-direction: column; gap: 12px;
|
||||
}
|
||||
.role-tag {
|
||||
font-size: 13px; font-weight: 600; padding: 8px 12px;
|
||||
border-radius: 6px; text-align: center;
|
||||
}
|
||||
.flow-panel { flex: 1; min-width: 0; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Infographic Templates
|
||||
|
||||
The following templates are for non-flowchart information visualization.
|
||||
|
||||
### Template: KPI Dashboard Cards
|
||||
|
||||
```css
|
||||
.kpi-grid {
|
||||
display: grid; grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px; margin-bottom: 32px;
|
||||
}
|
||||
.kpi-card {
|
||||
background: var(--surface); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 24px; text-align: center;
|
||||
}
|
||||
.kpi-label { font-size: 13px; color: var(--text-sub); margin-bottom: 8px; }
|
||||
.kpi-value { font-size: 32px; font-weight: 700; }
|
||||
.kpi-change { font-size: 14px; font-weight: 600; margin-top: 8px; }
|
||||
.kpi-change.up { color: var(--positive); }
|
||||
.kpi-change.down { color: var(--negative); }
|
||||
```
|
||||
|
||||
### Template: CSS Bar Chart (Pure CSS, No JS)
|
||||
|
||||
```css
|
||||
.bar-chart {
|
||||
display: flex; align-items: flex-end; gap: 16px;
|
||||
height: 300px; padding: 0 20px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.bar-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; }
|
||||
.bar { width: 100%; max-width: 60px; border-radius: 6px 6px 0 0; background: var(--border); }
|
||||
.bar.highlight { background: var(--blue); }
|
||||
.bar-label { font-size: 12px; color: var(--text-sub); }
|
||||
.bar-value { font-size: 11px; color: var(--text-muted); font-weight: 600; }
|
||||
```
|
||||
|
||||
### Template: Gradient Background Infographic Header
|
||||
|
||||
```css
|
||||
.hero-header {
|
||||
background: linear-gradient(135deg, #1E293B 0%, #0F172A 100%);
|
||||
border-radius: 16px; padding: 48px; color: white; margin-bottom: 32px;
|
||||
}
|
||||
.hero-header h1 { font-size: 28px; font-weight: 700; margin-bottom: 12px; }
|
||||
.hero-header p { font-size: 15px; color: #94A3B8; max-width: 600px; line-height: 1.6; }
|
||||
.hero-badge {
|
||||
display: inline-block; background: rgba(59,130,246,0.15);
|
||||
color: #60A5FA; font-size: 12px; font-weight: 600;
|
||||
padding: 4px 12px; border-radius: 20px; margin-bottom: 16px;
|
||||
}
|
||||
```
|
||||
|
||||
### Template: Data Card + Mini Sparkline
|
||||
|
||||
```html
|
||||
<div class="metric-card">
|
||||
<div class="metric-info">
|
||||
<div class="metric-title">月活用户</div>
|
||||
<div class="metric-value">34,521</div>
|
||||
<div class="metric-change" style="color: var(--positive)">↑ +18.2%</div>
|
||||
</div>
|
||||
<div class="metric-sparkline">
|
||||
<svg width="120" height="40" viewBox="0 0 120 40">
|
||||
<polyline fill="none" stroke="var(--blue)" stroke-width="2"
|
||||
points="0,35 15,30 30,25 45,28 60,20 75,15 90,12 105,8 120,5" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.metric-card {
|
||||
background: white; border: 1px solid var(--border);
|
||||
border-radius: 16px; padding: 28px;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.metric-title { font-size: 13px; color: var(--text-sub); }
|
||||
.metric-value { font-size: 36px; font-weight: 700; margin: 4px 0; }
|
||||
.metric-change { font-size: 14px; font-weight: 600; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Connector Rules
|
||||
|
||||
### Many-to-One Convergence
|
||||
|
||||
When multiple lines converge into one node, use the "merge first, then enter" pattern:
|
||||
|
||||
```
|
||||
[A] ──┐
|
||||
[B] ──┤── → [目标]
|
||||
[C] ──┘
|
||||
```
|
||||
|
||||
Implementation: Source lines reach a relay x-coordinate, merge into one vertical line, then a single line enters the target.
|
||||
|
||||
### Cross-Layer Connector Avoidance
|
||||
|
||||
If other nodes block the path between two nodes, **do not draw a straight line through them**.
|
||||
|
||||
Priority:
|
||||
1. **Redesign hierarchy** (best) — Most cross-layer lines indicate hierarchy design issues; connect adjacent layers only
|
||||
2. **Detour line** — Route around middle nodes via canvas edge (offset 40px right)
|
||||
3. **Thread through gap** — If middle-layer node gap ≥ 40px, route through the gap
|
||||
|
||||
### Connector Alignment
|
||||
|
||||
- Multiple lines from the same node start at a consistent position on the node border
|
||||
- Use only one connector style per chart (right-angle/curved/straight), no mixing
|
||||
- Connector label positions must be consistent (all above line or all centered)
|
||||
|
||||
---
|
||||
|
||||
## Font Size Rules
|
||||
|
||||
| Element | Recommended | Minimum |
|
||||
|------|------|------|
|
||||
| Flowchart main node title | 16-18px | 14px |
|
||||
| Node description / subtext | 13-15px | 12px |
|
||||
| Connector labels | 12-14px | 11px |
|
||||
| Legend text | 13-14px | 12px |
|
||||
| Footnotes / watermark | 11-13px | 10px |
|
||||
| Phase titles | 16-18px | 16px |
|
||||
| Role labels | 13-14px | 12px |
|
||||
|
||||
**Not enough space → Enlarge canvas, don't shrink fonts.**
|
||||
|
||||
---
|
||||
|
||||
## Overflow Protection
|
||||
|
||||
1. **`#root { width: fit-content; min-width: 800px; }`** — Expands with content automatically
|
||||
2. **`overflow: hidden` / `overflow: clip` are forbidden**
|
||||
3. **Auto-resize viewport before Playwright screenshot** (see 3.1 screenshot script)
|
||||
4. **Canvas minimum width**:
|
||||
|
||||
| Layout Type | Minimum Width | Recommended |
|
||||
|---------|---------|------|
|
||||
| Single-column flowchart | 800px | 1000px |
|
||||
| Dual-panel / swimlane | 1400px | 1600-1800px |
|
||||
| Three-column / multi-panel | 1800px | 2000-2400px |
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
1. **Layout C used by default** — If this is a flowchart, verify you're using Layout C (Phased Vertical) unless there's a specific reason not to
|
||||
2. **Content complete** — Every node/step from the requirement is in the chart
|
||||
3. **No overlap** — No boxes covering boxes, no lines through boxes
|
||||
4. **Clear hierarchy** — Phase titles and sub-steps are instantly distinguishable
|
||||
5. **Colors reasonable** — Node backgrounds low-saturation, no children's-drawing palette
|
||||
6. **Connectors visible** — Connector color ≥ `#94A3B8`, arrow direction correct
|
||||
7. **Font sizes meet standards** — Check against font size table, nothing below minimum
|
||||
8. **Legend independent** — Not inside flow-grid, not obscured by any node
|
||||
9. **No clipping** — Padding ≥ 40px on all sides, all nodes and labels fully visible
|
||||
10. **Phase colors consistent** — Same hue family, no blue/brown/green/purple mix
|
||||
11. **Connectors don't pass through nodes** — Cross-layer lines use detour or redesign hierarchy
|
||||
12. **No scattered layout** — Phase titles MUST be inside group cards, NOT floating as isolated labels
|
||||
|
||||
---
|
||||
|
||||
## Playwright+CSS vs Other Approaches
|
||||
|
||||
| Capability | Playwright+CSS | matplotlib | ECharts |
|
||||
|------|---------------|------------|---------|
|
||||
| Gradients/shadows/rounded | ✅ Full CSS power | ❌ Limited | ⚠️ Partial |
|
||||
| Responsive layout | ✅ Flexbox/Grid | ❌ Fixed size | ⚠️ resize |
|
||||
| PNG/PDF export | ✅ Native | ✅ savefig | ⚠️ Needs Playwright |
|
||||
| Precise data charts | ⚠️ Manual | ✅ Built-in | ✅ Built-in |
|
||||
|
||||
**Best practice: CSS for layout and visual design, embed ECharts/SVG for precise charts.**
|
||||
Executable
+576
@@ -0,0 +1,576 @@
|
||||
# CSS Radial Grid Layout (Center-Outward Diagrams)
|
||||
|
||||
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
|
||||
|
||||
**For: SWOT analysis, Balanced Scorecard (BSC), Porter's Five Forces, PEST analysis, and any "center + 4-6 surrounding dimensions" diagram.**
|
||||
|
||||
**Core principle: Use flex rows to lock positions — model never calculates coordinates. Connectors are drawn by script reading bounding boxes.**
|
||||
|
||||
---
|
||||
|
||||
## When to Use This Template
|
||||
|
||||
| Diagram Type | Dimensions | Use This? |
|
||||
|-------------|-----------|-----------|
|
||||
| SWOT (Strengths/Weaknesses/Opportunities/Threats) | 4 quadrants | ✅ Layout B (2×2 Grid) |
|
||||
| Balanced Scorecard (Financial/Customer/Internal/Learning) | 4 dimensions | ✅ Layout A (Cross) |
|
||||
| Porter's Five Forces | 5 forces | ✅ Layout A (Cross + extra row) |
|
||||
| PEST (Political/Economic/Social/Technological) | 4 dimensions | ✅ Layout B (2×2 Grid) |
|
||||
| Competency wheel / capability map | 5-8 dimensions | ✅ Layout A with extra rows |
|
||||
| Anything with center + surrounding elements | 3-8 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Layout A: Cross Layout (4-6 Dimensions)
|
||||
|
||||
Best for: BSC, Porter's Five Forces, any "center with surrounding dimensions" structure.
|
||||
|
||||
### 🚫 FORBIDDEN: 3×3 CSS Grid Cross
|
||||
|
||||
**Do NOT use `grid-template-columns: Xpx Ypx Xpx` to place cards in a cross pattern.** The top/bottom cards in the center column will overflow into the side columns and overlap with left/right cards when content is longer than expected.
|
||||
|
||||
### ✅ REQUIRED: Three-Row Flex Layout
|
||||
|
||||
**Each row is an independent flex container. Rows cannot overlap each other — physically impossible.**
|
||||
|
||||
```
|
||||
Row 1 (top): [top dimension card] ← independent flex row, centered
|
||||
Row 2 (middle): [left card] [center] [right card] ← independent flex row, horizontal
|
||||
Row 3 (bottom): [bottom dimension card] ← independent flex row, centered
|
||||
```
|
||||
|
||||
### HTML + CSS
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
#root { width: fit-content; min-width: 900px; padding: 48px 60px; }
|
||||
|
||||
.diagram-title {
|
||||
font-size: 22px; font-weight: 700; color: #1F2937;
|
||||
text-align: center; margin-bottom: 6px;
|
||||
}
|
||||
.diagram-subtitle {
|
||||
font-size: 14px; color: #6B7280;
|
||||
text-align: center; margin-bottom: 40px;
|
||||
}
|
||||
|
||||
/* === Three-row flex layout: rows CANNOT overlap === */
|
||||
.cross-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Middle row: left card + center + right card */
|
||||
.middle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
/* Center node */
|
||||
.center-node {
|
||||
background: linear-gradient(135deg, #1E293B, #334155);
|
||||
color: white; font-size: 18px; font-weight: 700;
|
||||
padding: 24px 32px; border-radius: 14px;
|
||||
text-align: center; z-index: 2;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.center-sub {
|
||||
font-size: 12px; font-weight: 400; color: #94A3B8;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Dimension cards */
|
||||
.dim-card {
|
||||
background: #FFFFFF; border-radius: 12px;
|
||||
padding: 20px; border: 2px solid;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
z-index: 2;
|
||||
width: 260px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dim-card .dim-title {
|
||||
font-size: 15px; font-weight: 700; margin-bottom: 10px;
|
||||
padding-bottom: 8px; border-bottom: 2px solid;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.dim-card .dim-icon {
|
||||
width: 26px; height: 26px; border-radius: 6px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 13px; font-weight: 700; flex-shrink: 0;
|
||||
}
|
||||
.dim-card .dim-items { list-style: none; }
|
||||
.dim-card .dim-items li {
|
||||
font-size: 13px; color: #475569; padding: 4px 0 4px 18px;
|
||||
position: relative; line-height: 1.5;
|
||||
}
|
||||
.dim-card .dim-items li::before {
|
||||
content: ''; position: absolute; left: 0; top: 10px;
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Color variants — border + title border + bullet */
|
||||
.dim-blue { border-color: #3B82F6; }
|
||||
.dim-blue .dim-title { color: #1E40AF; border-color: #3B82F6; }
|
||||
.dim-blue .dim-icon { background: #DBEAFE; color: #1E40AF; }
|
||||
.dim-blue li::before { background: #3B82F6; }
|
||||
|
||||
.dim-green { border-color: #10B981; }
|
||||
.dim-green .dim-title { color: #065F46; border-color: #10B981; }
|
||||
.dim-green .dim-icon { background: #D1FAE5; color: #065F46; }
|
||||
.dim-green li::before { background: #10B981; }
|
||||
|
||||
.dim-amber { border-color: #F59E0B; }
|
||||
.dim-amber .dim-title { color: #92400E; border-color: #F59E0B; }
|
||||
.dim-amber .dim-icon { background: #FEF3C7; color: #92400E; }
|
||||
.dim-amber li::before { background: #F59E0B; }
|
||||
|
||||
.dim-purple { border-color: #8B5CF6; }
|
||||
.dim-purple .dim-title { color: #5B21B6; border-color: #8B5CF6; }
|
||||
.dim-purple .dim-icon { background: #EDE9FE; color: #5B21B6; }
|
||||
.dim-purple li::before { background: #8B5CF6; }
|
||||
|
||||
.dim-red { border-color: #EF4444; }
|
||||
.dim-red .dim-title { color: #991B1B; border-color: #EF4444; }
|
||||
.dim-red .dim-icon { background: #FEE2E2; color: #991B1B; }
|
||||
.dim-red li::before { background: #EF4444; }
|
||||
|
||||
.dim-cyan { border-color: #06B6D4; }
|
||||
.dim-cyan .dim-title { color: #155E75; border-color: #06B6D4; }
|
||||
.dim-cyan .dim-icon { background: #CFFAFE; color: #155E75; }
|
||||
.dim-cyan li::before { background: #06B6D4; }
|
||||
|
||||
/* SVG connector layer */
|
||||
.cross-connectors {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
pointer-events: none; z-index: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<div class="diagram-title">平衡计分卡四维评价体系</div>
|
||||
<div class="diagram-subtitle">基于战略目标的绩效管理框架</div>
|
||||
|
||||
<div class="cross-layout" id="crossLayout">
|
||||
<!-- SVG connectors drawn by script -->
|
||||
<svg class="cross-connectors" id="connSvg"></svg>
|
||||
|
||||
<!-- Row 1: top dimension -->
|
||||
<div class="dim-card dim-blue" data-pos="top">
|
||||
<div class="dim-title"><div class="dim-icon">F</div> 财务维度</div>
|
||||
<ul class="dim-items">
|
||||
<li>营收增长率</li>
|
||||
<li>利润率</li>
|
||||
<li>ROI</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Row 2: left + center + right -->
|
||||
<div class="middle-row">
|
||||
<div class="dim-card dim-green" data-pos="left">
|
||||
<div class="dim-title"><div class="dim-icon">I</div> 内部流程</div>
|
||||
<ul class="dim-items">
|
||||
<li>流程效率</li>
|
||||
<li>质量管控</li>
|
||||
<li>创新能力</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="center-node">战略目标</div>
|
||||
|
||||
<div class="dim-card dim-amber" data-pos="right">
|
||||
<div class="dim-title"><div class="dim-icon">C</div> 客户维度</div>
|
||||
<ul class="dim-items">
|
||||
<li>客户满意度</li>
|
||||
<li>市场份额</li>
|
||||
<li>客户留存率</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: bottom dimension -->
|
||||
<div class="dim-card dim-purple" data-pos="bottom">
|
||||
<div class="dim-title"><div class="dim-icon">L</div> 学习与成长</div>
|
||||
<ul class="dim-items">
|
||||
<li>员工能力提升</li>
|
||||
<li>信息系统建设</li>
|
||||
<li>组织文化</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function drawCrossConnectors() {
|
||||
const layout = document.getElementById('crossLayout');
|
||||
const svg = document.getElementById('connSvg');
|
||||
const gRect = layout.getBoundingClientRect();
|
||||
|
||||
svg.setAttribute('width', gRect.width);
|
||||
svg.setAttribute('height', gRect.height);
|
||||
svg.setAttribute('viewBox', `0 0 ${gRect.width} ${gRect.height}`);
|
||||
svg.innerHTML = '';
|
||||
|
||||
// Bidirectional arrow markers — eliminates direction ambiguity
|
||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||
defs.innerHTML = `
|
||||
<marker id="arrowEnd" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#94A3B8" />
|
||||
</marker>
|
||||
<marker id="arrowStart" markerWidth="8" markerHeight="6" refX="1" refY="3" orient="auto">
|
||||
<polygon points="8 0, 0 3, 8 6" fill="#94A3B8" />
|
||||
</marker>
|
||||
`;
|
||||
svg.appendChild(defs);
|
||||
|
||||
const center = layout.querySelector('.center-node');
|
||||
const cR = center.getBoundingClientRect();
|
||||
const cx = cR.left - gRect.left + cR.width / 2;
|
||||
const cy = cR.top - gRect.top + cR.height / 2;
|
||||
|
||||
// Draw connector from center edge to each card edge
|
||||
const cards = layout.querySelectorAll('.dim-card');
|
||||
cards.forEach(card => {
|
||||
const pos = card.dataset.pos;
|
||||
const r = card.getBoundingClientRect();
|
||||
const cardCx = r.left - gRect.left + r.width / 2;
|
||||
const cardCy = r.top - gRect.top + r.height / 2;
|
||||
|
||||
let x1, y1, x2, y2;
|
||||
switch (pos) {
|
||||
case 'top':
|
||||
x1 = cx; y1 = cR.top - gRect.top; // center top edge midpoint
|
||||
x2 = cardCx; y2 = r.bottom - gRect.top; // card bottom edge midpoint
|
||||
break;
|
||||
case 'bottom':
|
||||
x1 = cx; y1 = cR.bottom - gRect.top; // center bottom edge midpoint
|
||||
x2 = cardCx; y2 = r.top - gRect.top; // card top edge midpoint
|
||||
break;
|
||||
case 'left':
|
||||
x1 = cR.left - gRect.left; y1 = cy; // center left edge midpoint
|
||||
x2 = r.right - gRect.left; y2 = cardCy; // card right edge midpoint
|
||||
break;
|
||||
case 'right':
|
||||
x1 = cR.right - gRect.left; y1 = cy; // center right edge midpoint
|
||||
x2 = r.left - gRect.left; y2 = cardCy; // card left edge midpoint
|
||||
break;
|
||||
// --- 5+ dimension: bottom row with two cards side by side ---
|
||||
case 'bottom-left':
|
||||
x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT (not corner!)
|
||||
x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT (not corner!)
|
||||
x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT
|
||||
break;
|
||||
}
|
||||
|
||||
// 🚫 FORBIDDEN: drawing lines from center CORNERS (e.g. cR.left + cR.bottom)
|
||||
// All lines MUST originate from center EDGE MIDPOINTS (cx or cy)
|
||||
|
||||
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
||||
line.setAttribute('x1', x1); line.setAttribute('y1', y1);
|
||||
line.setAttribute('x2', x2); line.setAttribute('y2', y2);
|
||||
line.setAttribute('stroke', '#94A3B8');
|
||||
line.setAttribute('stroke-width', '2');
|
||||
line.setAttribute('stroke-dasharray', '6,4');
|
||||
line.setAttribute('marker-start', 'url(#arrowStart)');
|
||||
line.setAttribute('marker-end', 'url(#arrowEnd)');
|
||||
svg.appendChild(line);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => setTimeout(drawCrossConnectors, 300));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Key design decisions:**
|
||||
- **Three-row flex** instead of 3×3 Grid — rows physically cannot overlap
|
||||
- **Fixed card width (260px)** — prevents content-driven overflow
|
||||
- **`gap: 24px`** between rows, **`gap: 40px`** within middle row — generous spacing
|
||||
- **Center node uses `flex-shrink: 0`** — never collapses under pressure
|
||||
|
||||
### Adapting for 5+ Dimensions
|
||||
|
||||
For Porter's Five Forces (5 dimensions) or more:
|
||||
|
||||
```html
|
||||
<!-- Row 1: top -->
|
||||
<div class="dim-card dim-blue" data-pos="top">...</div>
|
||||
|
||||
<!-- Row 2: left + center + right -->
|
||||
<div class="middle-row">
|
||||
<div class="dim-card dim-green" data-pos="left">...</div>
|
||||
<div class="center-node">...</div>
|
||||
<div class="dim-card dim-amber" data-pos="right">...</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: bottom-left + bottom-right (two cards side by side) -->
|
||||
<div class="middle-row" style="gap: 40px;">
|
||||
<div class="dim-card dim-purple" data-pos="bottom-left">...</div>
|
||||
<div class="dim-card dim-red" data-pos="bottom-right">...</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
For the connector script, add cases for `bottom-left` and `bottom-right`:
|
||||
```javascript
|
||||
// 🚫 FORBIDDEN: using cR.left/cR.right as x1 — that draws from center CORNER, angle is ugly
|
||||
// ✅ CORRECT: always use cx (center bottom midpoint) as x1
|
||||
case 'bottom-left':
|
||||
x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT
|
||||
x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT
|
||||
break;
|
||||
case 'bottom-right':
|
||||
x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT
|
||||
x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT
|
||||
break;
|
||||
```
|
||||
|
||||
### Adapting for 3 Dimensions
|
||||
|
||||
Simply remove one side card from the middle row:
|
||||
|
||||
```html
|
||||
<!-- Row 1: top -->
|
||||
<div class="dim-card dim-blue" data-pos="top">...</div>
|
||||
|
||||
<!-- Row 2: center + right only -->
|
||||
<div class="middle-row">
|
||||
<div class="center-node">...</div>
|
||||
<div class="dim-card dim-amber" data-pos="right">...</div>
|
||||
</div>
|
||||
|
||||
<!-- Row 3: bottom -->
|
||||
<div class="dim-card dim-purple" data-pos="bottom">...</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout B: 2×2 Quadrant Grid (SWOT / PEST)
|
||||
|
||||
Best for: exactly 4 dimensions arranged as equal quadrants (no center node needed).
|
||||
|
||||
**This layout has NO center node — the four quadrants themselves tell the story.**
|
||||
|
||||
### HTML + CSS
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
#root { width: fit-content; min-width: 900px; padding: 48px 60px; }
|
||||
|
||||
.diagram-title {
|
||||
font-size: 22px; font-weight: 700; color: #1F2937;
|
||||
text-align: center; margin-bottom: 12px;
|
||||
}
|
||||
.diagram-subtitle {
|
||||
font-size: 14px; color: #6B7280;
|
||||
text-align: center; margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.quadrant-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.quadrant {
|
||||
border-radius: 12px; padding: 24px;
|
||||
border: 1.5px solid;
|
||||
min-height: 200px;
|
||||
}
|
||||
.quadrant .q-title {
|
||||
font-size: 16px; font-weight: 700; margin-bottom: 14px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.quadrant .q-icon {
|
||||
width: 28px; height: 28px; border-radius: 6px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 14px; font-weight: 700;
|
||||
}
|
||||
.quadrant .q-items { list-style: none; }
|
||||
.quadrant .q-items li {
|
||||
font-size: 13px; padding: 5px 0 5px 18px;
|
||||
position: relative; line-height: 1.5;
|
||||
}
|
||||
.quadrant .q-items li::before {
|
||||
content: ''; position: absolute; left: 0; top: 11px;
|
||||
width: 6px; height: 6px; border-radius: 50%;
|
||||
}
|
||||
|
||||
/* SWOT colors — low-sat backgrounds, sat borders */
|
||||
.q-strengths { background: #F0FDF4; border-color: #86EFAC; }
|
||||
.q-strengths .q-title { color: #065F46; }
|
||||
.q-strengths .q-icon { background: #D1FAE5; color: #065F46; }
|
||||
.q-strengths li { color: #1F2937; }
|
||||
.q-strengths li::before { background: #10B981; }
|
||||
|
||||
.q-weaknesses { background: #FEF2F2; border-color: #FECACA; }
|
||||
.q-weaknesses .q-title { color: #991B1B; }
|
||||
.q-weaknesses .q-icon { background: #FEE2E2; color: #991B1B; }
|
||||
.q-weaknesses li { color: #1F2937; }
|
||||
.q-weaknesses li::before { background: #EF4444; }
|
||||
|
||||
.q-opportunities { background: #EFF6FF; border-color: #93C5FD; }
|
||||
.q-opportunities .q-title { color: #1E40AF; }
|
||||
.q-opportunities .q-icon { background: #DBEAFE; color: #1E40AF; }
|
||||
.q-opportunities li { color: #1F2937; }
|
||||
.q-opportunities li::before { background: #3B82F6; }
|
||||
|
||||
.q-threats { background: #FFF7ED; border-color: #FDE68A; }
|
||||
.q-threats .q-title { color: #92400E; }
|
||||
.q-threats .q-icon { background: #FEF3C7; color: #92400E; }
|
||||
.q-threats li { color: #1F2937; }
|
||||
.q-threats li::before { background: #F59E0B; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<div class="diagram-title">SWOT 分析</div>
|
||||
<div class="diagram-subtitle">企业战略定位评估</div>
|
||||
|
||||
<div class="quadrant-grid">
|
||||
<div class="quadrant q-strengths">
|
||||
<div class="q-title"><div class="q-icon">S</div> 优势 Strengths</div>
|
||||
<ul class="q-items">
|
||||
<li>核心技术领先</li>
|
||||
<li>品牌知名度高</li>
|
||||
<li>供应链成熟</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="quadrant q-weaknesses">
|
||||
<div class="q-title"><div class="q-icon">W</div> 劣势 Weaknesses</div>
|
||||
<ul class="q-items">
|
||||
<li>国际化经验不足</li>
|
||||
<li>产品线单一</li>
|
||||
<li>人才储备有限</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="quadrant q-opportunities">
|
||||
<div class="q-title"><div class="q-icon">O</div> 机会 Opportunities</div>
|
||||
<ul class="q-items">
|
||||
<li>新兴市场需求增长</li>
|
||||
<li>政策利好</li>
|
||||
<li>技术融合趋势</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="quadrant q-threats">
|
||||
<div class="q-title"><div class="q-icon">T</div> 威胁 Threats</div>
|
||||
<ul class="q-items">
|
||||
<li>竞争加剧</li>
|
||||
<li>原材料价格波动</li>
|
||||
<li>法规变化风险</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**No connectors needed** — the 2×2 grid itself communicates the four-quadrant relationship. Adding arrows would be visual noise.
|
||||
|
||||
---
|
||||
|
||||
## Connector Rules
|
||||
|
||||
1. **Connectors are ALWAYS drawn by script reading bounding boxes** — never hardcode x/y values in HTML/CSS
|
||||
2. **Straight lines only** (horizontal or vertical) — no diagonal lines unless top/bottom cards are offset from center
|
||||
3. **Dashed lines** (`stroke-dasharray: 6,4`) for conceptual relationships
|
||||
4. **Solid lines** for causal/sequential relationships
|
||||
5. **Arrow direction**: model chooses based on diagram semantics — pick ONE style per diagram, don't mix:
|
||||
- **Outward** (`marker-end` only): center influences/drives dimensions (e.g. BSC: strategy → dimensions)
|
||||
- **Inward** (`marker-start` only): dimensions report/pressure center (e.g. Porter: forces → competition)
|
||||
- **Bidirectional** (`marker-start` + `marker-end`): mutual influence (e.g. feedback loops)
|
||||
6. **🚫 All lines MUST originate from center EDGE MIDPOINTS** (cx or cy) — never from center corners
|
||||
7. **Line color**: `#94A3B8` (gray-blue) — never use dimension-specific colors for connectors (visual chaos)
|
||||
|
||||
### SVG Arrow Markers (copy-paste into defs)
|
||||
|
||||
```javascript
|
||||
// Include both markers; use only what the diagram needs
|
||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||
defs.innerHTML = `
|
||||
<marker id="arrowEnd" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#94A3B8" />
|
||||
</marker>
|
||||
<marker id="arrowStart" markerWidth="8" markerHeight="6" refX="1" refY="3" orient="auto">
|
||||
<polygon points="8 0, 0 3, 8 6" fill="#94A3B8" />
|
||||
</marker>
|
||||
`;
|
||||
svg.appendChild(defs);
|
||||
|
||||
// Outward (center → card):
|
||||
line.setAttribute('marker-end', 'url(#arrowEnd)');
|
||||
|
||||
// Inward (card → center):
|
||||
line.setAttribute('marker-start', 'url(#arrowStart)');
|
||||
|
||||
// Bidirectional:
|
||||
line.setAttribute('marker-start', 'url(#arrowStart)');
|
||||
line.setAttribute('marker-end', 'url(#arrowEnd)');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Content Rules
|
||||
|
||||
1. **Each dimension card: max 6 bullet items** — more than 6 → group into sub-categories or use a separate detail table
|
||||
2. **Bullet text: max 15 Chinese characters per line** — longer text wraps naturally (word-break: break-word)
|
||||
3. **Center node text: max 8 Chinese characters** — keep it a short label, not a sentence
|
||||
4. **Dimension title: max 10 Chinese characters** — concise category name
|
||||
|
||||
---
|
||||
|
||||
## Font Size Rules
|
||||
|
||||
| Element | Size | Weight |
|
||||
|---------|------|--------|
|
||||
| Diagram title | 22px | 700 |
|
||||
| Center node | 18px | 700 |
|
||||
| Dimension title | 15-16px | 700 |
|
||||
| Bullet items | 13px | 400 |
|
||||
| Subtitle/footnote | 14px | 400 |
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
1. **No overlap** — all dimension cards fully visible, none clipped or covering another
|
||||
2. **Connectors point to card edges** — not to random points in space
|
||||
3. **Center node visually dominant** — largest, darkest, or most contrast
|
||||
4. **Dimension cards visually equal** — similar size, similar padding, no one card 3× larger
|
||||
5. **Colors follow scheme** — each dimension has its own color (border + title), backgrounds stay pale (white or near-white)
|
||||
6. **Layout is centered** — equal margins on all sides
|
||||
7. **Canvas large enough** — min 900px wide for cross layout, min 800px for 2×2 quadrant
|
||||
Executable
+324
@@ -0,0 +1,324 @@
|
||||
# Seaborn Template Library
|
||||
|
||||
> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.**
|
||||
|
||||
|
||||
Seaborn is built on top of matplotlib, specializing in **statistical visualization** — distributions, regression, categorical comparisons, etc.
|
||||
Its default styles are already much better looking than matplotlib, but still need tuning to reach professional standards.
|
||||
|
||||
## Environment Setup
|
||||
|
||||
```python
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
# ═══ Chinese Font Setup ═══
|
||||
# Font path: adjust for your system. Common locations:
|
||||
# macOS: '/System/Library/Fonts/Supplemental/SimHei.ttf'
|
||||
# Linux: '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'
|
||||
# Custom: './fonts/SimHei.ttf'
|
||||
import os
|
||||
SIMHEI_PATH = os.environ.get('SIMHEI_FONT', '/System/Library/Fonts/Supplemental/SimHei.ttf')
|
||||
matplotlib.font_manager.fontManager.addfont(SIMHEI_PATH)
|
||||
|
||||
# Seaborn theme + custom overrides
|
||||
sns.set_theme(style='whitegrid', font='SimHei', rc={
|
||||
'axes.unicode_minus': False,
|
||||
'figure.facecolor': '#FFFFFF',
|
||||
'axes.facecolor': '#FFFFFF',
|
||||
'axes.edgecolor': '#E5E7EB',
|
||||
'axes.linewidth': 0.8,
|
||||
'axes.spines.top': False,
|
||||
'axes.spines.right': False,
|
||||
'grid.color': '#F3F4F6',
|
||||
'grid.alpha': 0.5,
|
||||
'grid.linewidth': 0.5,
|
||||
'xtick.major.size': 0,
|
||||
'ytick.major.size': 0,
|
||||
'axes.titlesize': 16,
|
||||
'axes.titleweight': 'bold',
|
||||
'axes.titlepad': 16,
|
||||
'legend.frameon': False,
|
||||
'figure.dpi': 200,
|
||||
'savefig.dpi': 200,
|
||||
'savefig.bbox': 'tight',
|
||||
})
|
||||
|
||||
# Colors (consistent with matplotlib.md)
|
||||
COOL = ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981']
|
||||
CB_SAFE = ['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377']
|
||||
```
|
||||
|
||||
## Seaborn Palette Setup
|
||||
|
||||
```python
|
||||
# Method 1: use hex list directly
|
||||
sns.set_palette(COOL)
|
||||
|
||||
# Method 2: register custom palette
|
||||
from matplotlib.colors import ListedColormap
|
||||
bc_palette = ListedColormap(COOL, name='charts')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 1: Distribution Plot (Histogram + KDE)
|
||||
|
||||
**Scenario**: View data distribution shape, detect skewness and outliers.
|
||||
|
||||
```python
|
||||
def dist_plot(data, title, xlabel, color='#3B82F6', save_path='dist.png'):
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
sns.histplot(data, kde=True, color=color, edgecolor='white',
|
||||
linewidth=0.5, alpha=0.7, ax=ax)
|
||||
|
||||
# Bold the KDE line
|
||||
for line in ax.get_lines():
|
||||
line.set_linewidth(2.5)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
ax.set_xlabel(xlabel)
|
||||
ax.set_ylabel('Frequency')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 2: Box Plot (Categorical Comparison)
|
||||
|
||||
**Scenario**: Compare distribution characteristics across groups (median, quartiles, outliers).
|
||||
|
||||
```python
|
||||
def box_compare(df, x_col, y_col, title, palette=None, save_path='box.png'):
|
||||
if palette is None:
|
||||
palette = COOL
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
sns.boxplot(data=df, x=x_col, y=y_col, palette=palette,
|
||||
width=0.5, linewidth=1.2, fliersize=4,
|
||||
boxprops=dict(edgecolor='white'),
|
||||
medianprops=dict(color='white', linewidth=2),
|
||||
ax=ax)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
### Box Plot + Label Avoidance (For Complex Scenarios)
|
||||
|
||||
When you need to annotate outliers, specific data points, or group names on a box plot, **label avoidance is required**:
|
||||
|
||||
```python
|
||||
def box_annotated(df, x_col, y_col, title, annotations=None,
|
||||
palette=None, save_path='box_annotated.png'):
|
||||
"""
|
||||
annotations: [{'x': 0, 'y': 45, 'text': '产线A 异常'}, ...]
|
||||
"""
|
||||
from adjustText import adjust_text
|
||||
|
||||
if palette is None:
|
||||
palette = COOL
|
||||
|
||||
fig, ax = plt.subplots(figsize=(12, 7)) # Slightly larger canvas, leave room for annotations
|
||||
|
||||
sns.boxplot(data=df, x=x_col, y=y_col, palette=palette,
|
||||
width=0.5, linewidth=1.2, fliersize=4,
|
||||
boxprops=dict(edgecolor='white'),
|
||||
medianprops=dict(color='white', linewidth=2),
|
||||
ax=ax)
|
||||
|
||||
# Annotation text — use adjustText for auto-avoidance
|
||||
if annotations:
|
||||
texts = []
|
||||
for ann in annotations:
|
||||
t = ax.text(ann['x'], ann['y'], ann['text'],
|
||||
fontsize=9, color='#374151',
|
||||
bbox=dict(boxstyle='round,pad=0.3',
|
||||
facecolor='#FFF7ED', edgecolor='#F59E0B',
|
||||
alpha=0.9))
|
||||
texts.append(t)
|
||||
|
||||
adjust_text(texts, ax=ax,
|
||||
arrowprops=dict(arrowstyle='->', color='#9CA3AF', lw=0.8),
|
||||
force_text=(0.8, 1.0),
|
||||
force_points=(0.5, 0.8))
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
### Box Plot + Colorbar (e.g., MTTR Analysis)
|
||||
|
||||
When a box plot needs to work with a colorbar (e.g., colored by dimension), you must leave enough space for the colorbar:
|
||||
|
||||
```python
|
||||
def box_with_colorbar(df, x_col, y_col, color_col, title,
|
||||
save_path='box_cbar.png'):
|
||||
import matplotlib.gridspec as gridspec
|
||||
from matplotlib.colors import Normalize
|
||||
from matplotlib.cm import ScalarMappable
|
||||
|
||||
fig = plt.figure(figsize=(14, 7), constrained_layout=True)
|
||||
gs = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[1, 0.03], wspace=0.05)
|
||||
|
||||
ax = fig.add_subplot(gs[0, 0])
|
||||
cbar_ax = fig.add_subplot(gs[0, 1])
|
||||
|
||||
sns.boxplot(data=df, x=x_col, y=y_col, ax=ax,
|
||||
width=0.5, linewidth=1.2)
|
||||
|
||||
# Colorbar in its own subplot, won't obscure box plot
|
||||
norm = Normalize(vmin=df[color_col].min(), vmax=df[color_col].max())
|
||||
sm = ScalarMappable(norm=norm, cmap='Blues')
|
||||
fig.colorbar(sm, cax=cbar_ax, label=color_col)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
fig.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 3: Violin Plot
|
||||
|
||||
**Scenario**: Enhanced version of box plot, showing distribution density simultaneously.
|
||||
|
||||
```python
|
||||
def violin_plot(df, x_col, y_col, title, palette=None, save_path='violin.png'):
|
||||
if palette is None:
|
||||
palette = COOL
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
|
||||
sns.violinplot(data=df, x=x_col, y=y_col, palette=palette,
|
||||
inner='box', linewidth=1, ax=ax)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 4: Regression Scatter Plot
|
||||
|
||||
**Scenario**: Two-variable relationship + linear regression + confidence interval.
|
||||
|
||||
```python
|
||||
def reg_plot(df, x_col, y_col, title, color='#3B82F6', save_path='reg.png'):
|
||||
fig, ax = plt.subplots(figsize=(10, 7))
|
||||
|
||||
sns.regplot(data=df, x=x_col, y=y_col, color=color,
|
||||
scatter_kws={'s': 50, 'alpha': 0.6, 'edgecolor': 'white', 'linewidth': 1},
|
||||
line_kws={'linewidth': 2},
|
||||
ax=ax)
|
||||
|
||||
ax.set_title(title, loc='left')
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 5: Correlation Heatmap
|
||||
|
||||
**Scenario**: Correlation coefficient matrix among multiple variables.
|
||||
|
||||
```python
|
||||
def corr_heatmap(df, title, cmap='RdBu_r', save_path='corr.png'):
|
||||
corr = df.corr()
|
||||
|
||||
fig, ax = plt.subplots(figsize=(10, 8))
|
||||
|
||||
# Show only lower triangle
|
||||
mask = np.triu(np.ones_like(corr, dtype=bool))
|
||||
|
||||
sns.heatmap(corr, mask=mask, cmap=cmap, center=0,
|
||||
vmin=-1, vmax=1, annot=True, fmt='.2f',
|
||||
square=True, linewidths=1, linecolor='white',
|
||||
cbar_kws={'shrink': 0.8, 'label': 'Correlation'},
|
||||
annot_kws={'size': 9},
|
||||
ax=ax)
|
||||
|
||||
ax.set_title(title, loc='left', pad=16)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 6: Pair Plot
|
||||
|
||||
**Scenario**: Overview of pairwise relationships among multiple variables (distribution on diagonal, scatter plots elsewhere).
|
||||
|
||||
```python
|
||||
def pair_plot(df, hue_col=None, palette=None, save_path='pair.png'):
|
||||
if palette is None:
|
||||
palette = COOL
|
||||
|
||||
g = sns.pairplot(df, hue=hue_col, palette=palette,
|
||||
diag_kind='kde', plot_kws={'alpha': 0.6, 's': 30},
|
||||
height=2.5, aspect=1)
|
||||
|
||||
g.figure.suptitle('Pairwise Variable Relationships', y=1.02, fontsize=16, fontweight='bold')
|
||||
|
||||
g.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template 7: Facet Grid (FacetGrid)
|
||||
|
||||
**Scenario**: Split into multiple subplots by categorical variable for comparison.
|
||||
|
||||
```python
|
||||
def facet_hist(df, col_var, value_col, title, color='#3B82F6', save_path='facet.png'):
|
||||
g = sns.FacetGrid(df, col=col_var, col_wrap=3, height=3.5, aspect=1.3)
|
||||
g.map(sns.histplot, value_col, color=color, edgecolor='white',
|
||||
linewidth=0.5, alpha=0.7, kde=True)
|
||||
|
||||
g.set_titles('{col_name}', fontsize=12, fontweight='bold')
|
||||
g.figure.suptitle(title, y=1.02, fontsize=16, fontweight='bold')
|
||||
|
||||
g.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight')
|
||||
plt.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Seaborn vs matplotlib Selection Guide
|
||||
|
||||
| What to Plot | Use Seaborn | Use matplotlib |
|
||||
|---------|-----------|--------------|
|
||||
| Histogram/KDE distribution | ✅ `histplot` / `kdeplot` | Works, but more code |
|
||||
| Box plot/Violin plot | ✅ One-liner | Works, but rougher style |
|
||||
| Regression scatter + confidence interval | ✅ `regplot` auto-calculates | Manual fitting + plotting |
|
||||
| Correlation heatmap | ✅ `heatmap` + mask | Manual `imshow` tedious |
|
||||
| Pairwise relationship matrix | ✅ `pairplot` unique | No equivalent |
|
||||
| Facet grid | ✅ `FacetGrid` | `plt.subplots` manual loop |
|
||||
| Regular bar chart | Less flexible than matplotlib | ✅ More control |
|
||||
| Line trend chart | Less than matplotlib | ✅ More control |
|
||||
| Custom annotations/arrows | Not suitable | ✅ `ax.annotate` |
|
||||
|
||||
**Principle: Use Seaborn for statistical charts, matplotlib for customized charts.**
|
||||
Executable
+228
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env bash
|
||||
# ---
|
||||
# name: charts-setup
|
||||
# author: Z.AI
|
||||
# version: "1.0"
|
||||
# description: Environment setup for the Charts skill. Checks and installs all required dependencies.
|
||||
# ---
|
||||
#
|
||||
# Installs only dependencies required by the Charts skill.
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
|
||||
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||
fail() { echo -e " ${RED}✗${NC} $1"; }
|
||||
warn() { echo -e " ${YELLOW}○${NC} $1"; }
|
||||
info() { echo -e " ${BLUE}→${NC} $1"; }
|
||||
|
||||
echo "============================================"
|
||||
echo " Charts Skill — Environment Setup"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
OS="$(uname -s)"
|
||||
ARCH="$(uname -m)"
|
||||
echo "Platform: $OS $ARCH"
|
||||
echo ""
|
||||
|
||||
# ── 0. macOS: Homebrew ──
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
echo "--- Homebrew (macOS package manager) ---"
|
||||
if command -v brew &>/dev/null; then
|
||||
BREW_VER=$(brew --version 2>/dev/null | head -1)
|
||||
ok "brew ($BREW_VER)"
|
||||
else
|
||||
fail "brew not found — most dependencies below need Homebrew on macOS"
|
||||
info "Install: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
|
||||
fi
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── 1. Python 3 ──
|
||||
echo "--- Python ---"
|
||||
if command -v python3 &>/dev/null; then
|
||||
PY_VER=$(python3 --version 2>&1)
|
||||
ok "python3 ($PY_VER)"
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
PY_PATH=$(which python3 2>/dev/null)
|
||||
if [[ "$PY_PATH" == "/usr/bin/python3" ]]; then
|
||||
warn "Using macOS system Python (limited). Recommend: brew install python3"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
fail "python3 not found"
|
||||
case "$OS" in
|
||||
Darwin) info "Install: brew install python3" ;;
|
||||
Linux) info "Install: sudo apt install python3 python3-pip (Debian/Ubuntu)"
|
||||
info " sudo dnf install python3 python3-pip (Fedora/RHEL)" ;;
|
||||
*) info "Install: https://www.python.org/downloads/" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ── 2. pip ──
|
||||
echo ""
|
||||
echo "--- pip ---"
|
||||
if python3 -m pip --version &>/dev/null 2>&1; then
|
||||
PIP_VER=$(python3 -m pip --version 2>/dev/null | head -1)
|
||||
ok "pip ($PIP_VER)"
|
||||
else
|
||||
fail "pip not found"
|
||||
case "$OS" in
|
||||
Darwin) info "Install: python3 -m ensurepip --upgrade"
|
||||
info " or: brew install python3 (includes pip)" ;;
|
||||
Linux) info "Install: sudo apt install python3-pip (Debian/Ubuntu)" ;;
|
||||
*) info "Install: python3 -m ensurepip --upgrade" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ── 3. Python packages (matplotlib / seaborn for data charts) ──
|
||||
echo ""
|
||||
echo "--- Python Packages (Data Charts) ---"
|
||||
PY_PKGS=(
|
||||
"matplotlib:matplotlib"
|
||||
"seaborn:seaborn"
|
||||
"numpy:numpy"
|
||||
"adjustText:adjustText"
|
||||
)
|
||||
|
||||
MISSING_PY=()
|
||||
for entry in "${PY_PKGS[@]}"; do
|
||||
mod="${entry%%:*}"
|
||||
pkg="${entry##*:}"
|
||||
if python3 -c "import $mod" 2>/dev/null; then
|
||||
ver=$(python3 -c "import $mod; print(getattr($mod, '__version__', 'installed'))" 2>/dev/null)
|
||||
ok "$pkg ($ver)"
|
||||
else
|
||||
fail "$pkg not installed"
|
||||
MISSING_PY+=("$pkg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MISSING_PY[@]} -gt 0 ]; then
|
||||
echo ""
|
||||
if [ -t 0 ]; then
|
||||
read -p " Install missing Python packages? [Y/n] " -n 1 -r REPLY
|
||||
echo ""
|
||||
REPLY=${REPLY:-Y}
|
||||
else
|
||||
warn "Non-interactive mode — skipping auto-install. Run interactively or install manually."
|
||||
REPLY=N
|
||||
fi
|
||||
if [[ ! $REPLY =~ ^[Nn]$ ]]; then
|
||||
python3 -m pip install -q "${MISSING_PY[@]}" 2>/dev/null \
|
||||
|| python3 -m pip install -q --user "${MISSING_PY[@]}" 2>/dev/null \
|
||||
|| python3 -m pip install -q --break-system-packages "${MISSING_PY[@]}" 2>/dev/null \
|
||||
|| { fail "pip install failed. Try manually: pip install ${MISSING_PY[*]}"; }
|
||||
ok "Installed: ${MISSING_PY[*]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 4. Node.js ──
|
||||
echo ""
|
||||
echo "--- Node.js (Interactive Charts & Diagrams) ---"
|
||||
if command -v node &>/dev/null; then
|
||||
NODE_VER=$(node --version)
|
||||
ok "node ($NODE_VER)"
|
||||
else
|
||||
fail "node not found"
|
||||
case "$OS" in
|
||||
Darwin) info "Install: brew install node" ;;
|
||||
Linux) info "Install: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -"
|
||||
info " sudo apt install -y nodejs" ;;
|
||||
*) info "Install: https://nodejs.org/" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ── 5. npm ──
|
||||
echo ""
|
||||
echo "--- npm ---"
|
||||
if command -v npm &>/dev/null; then
|
||||
NPM_VER=$(npm --version 2>/dev/null)
|
||||
ok "npm ($NPM_VER)"
|
||||
else
|
||||
fail "npm not found"
|
||||
case "$OS" in
|
||||
Darwin) info "Install: brew install node (includes npm)" ;;
|
||||
Linux) info "Install: comes with nodejs" ;;
|
||||
*) info "Install: https://nodejs.org/" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# ── 6. Playwright + Chromium (for HTML→PNG/PDF rendering) ──
|
||||
echo ""
|
||||
echo "--- Playwright (Structural Diagrams & HTML Charts) ---"
|
||||
if node -e "require('playwright')" 2>/dev/null; then
|
||||
PW_VER=$(node -e "console.log(require('playwright/package.json').version)" 2>/dev/null)
|
||||
ok "playwright ($PW_VER)"
|
||||
else
|
||||
fail "playwright not installed"
|
||||
info "Install: npm install -g playwright"
|
||||
fi
|
||||
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
PW_CACHE="$HOME/Library/Caches/ms-playwright"
|
||||
else
|
||||
PW_CACHE="$HOME/.cache/ms-playwright"
|
||||
fi
|
||||
if ls "$PW_CACHE"/chromium-* &>/dev/null 2>&1; then
|
||||
CR_DIR=$(ls -d "$PW_CACHE"/chromium-* 2>/dev/null | tail -1)
|
||||
ok "chromium ($(basename "$CR_DIR"))"
|
||||
else
|
||||
fail "chromium not installed"
|
||||
info "Install: npx playwright install chromium"
|
||||
if [ "$OS" = "Linux" ]; then
|
||||
info " npx playwright install-deps (system libs, needs sudo)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 7. CJK Fonts (for Chinese chart labels) ──
|
||||
echo ""
|
||||
echo "--- CJK Fonts (Chinese text in charts) ---"
|
||||
CJK_FOUND=false
|
||||
|
||||
# Check matplotlib registered fonts
|
||||
if python3 -c "
|
||||
import matplotlib.font_manager as fm
|
||||
fonts = [f.name for f in fm.fontManager.ttflist]
|
||||
if 'SimHei' in fonts or 'Heiti SC' in fonts or 'Noto Sans CJK' in fonts or 'PingFang SC' in fonts:
|
||||
print('ok')
|
||||
else:
|
||||
print('missing')
|
||||
" 2>/dev/null | grep -q "ok"; then
|
||||
ok "CJK font registered in matplotlib (SimHei/Heiti SC/Noto Sans CJK/PingFang SC)"
|
||||
CJK_FOUND=true
|
||||
fi
|
||||
|
||||
# Check system CJK fonts
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
if ls /System/Library/Fonts/PingFang.ttc &>/dev/null 2>&1 \
|
||||
|| ls /System/Library/Fonts/STHeiti*.ttc &>/dev/null 2>&1; then
|
||||
ok "macOS CJK system fonts available (PingFang/STHeiti)"
|
||||
CJK_FOUND=true
|
||||
fi
|
||||
elif [ "$OS" = "Linux" ]; then
|
||||
if fc-list :lang=zh 2>/dev/null | head -1 | grep -q .; then
|
||||
ok "system CJK fonts available (fc-list)"
|
||||
CJK_FOUND=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$CJK_FOUND" = false ]; then
|
||||
warn "No CJK font detected — Chinese labels may show as □"
|
||||
info "The skill configures font fallback via rcParams at runtime."
|
||||
info "Ensure a CJK font file exists (e.g., SimHei.ttf) and is registered."
|
||||
if [ "$OS" = "Darwin" ]; then
|
||||
info "macOS ships with PingFang — try: plt.rcParams['font.sans-serif'] = ['PingFang SC', 'DejaVu Sans']"
|
||||
elif [ "$OS" = "Linux" ]; then
|
||||
info "Install: sudo apt install fonts-noto-cjk"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Summary ──
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Setup complete."
|
||||
echo " Data charts: matplotlib + seaborn"
|
||||
echo " Structural diagrams: Playwright + CSS"
|
||||
echo " Interactive charts: ECharts / D3.js via Node.js"
|
||||
echo "============================================"
|
||||
Executable
+210
@@ -0,0 +1,210 @@
|
||||
---
|
||||
name: cheat-sheet
|
||||
description: 将 PDF/Word/Markdown 学习资料转化为精炼的知识浓缩卡文档。支持三种风格(知识点速查卡/思维导图式/Q&A式),输出双栏小字 PDF。当用户说"生成知识浓缩卡"、"生成 Cheatsheet"、"帮我做个速查表"、"把这个资料整理成一页纸"、"做个知识卡片"时触发。**不处理**:基于材料出题(→ quiz-mastery)、长期学习项目(→ study-buddy)。
|
||||
---
|
||||
|
||||
# Cheatsheet 生成器
|
||||
|
||||
## 你做什么
|
||||
|
||||
把用户的学习资料(PDF/Word/Markdown/文本)压缩成一份**双栏、小字、信息密度高**的 Cheatsheet 文档(PDF 格式)。
|
||||
|
||||
**核心原则:只保留干货,砍掉废话。**
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 铁律:避免重复创建
|
||||
|
||||
生成 Cheatsheet 前,**先扫描输出目录**检查是否已有同主题文件:
|
||||
- **已存在** → 问用户"已经有一份 [文件名] 了,覆盖 / 新建一份 / 跳过?" 等用户回复再动手
|
||||
- **不存在** → 直接生成
|
||||
- **绝不**未经询问直接覆盖
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
### 步骤 1:接收资料
|
||||
|
||||
用户提供学习资料文件,支持格式:
|
||||
- `.pdf` — 调 PDF skill 的 process 路线提取文本
|
||||
- `.docx` — 调 PDF skill 的 process 路线提取文本
|
||||
- `.md` / `.txt` — 直接读取
|
||||
|
||||
**提取文本命令:**
|
||||
```bash
|
||||
# PDF 提取文本
|
||||
python3 "$PDF_SCRIPTS/pdf.py" extract.text <file_path>
|
||||
|
||||
# Word 转 PDF 后提取(如需要)
|
||||
python3 "$PDF_SCRIPTS/pdf.py" convert.office <file_path>
|
||||
```
|
||||
|
||||
其中 `$PDF_SCRIPTS` 为 PDF skill 的 scripts 目录路径,需从 PDF skill 的 SKILL.md 位置推导。
|
||||
|
||||
如果用户没有提供文件而是直接贴了文本内容,跳过提取步骤,直接用文本。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 2:选择风格
|
||||
|
||||
**必须询问用户**想要哪种风格(不要自己替用户选):
|
||||
|
||||
向用户展示以下选项:
|
||||
1. 📋 **知识点速查卡** — 核心概念 + 定义 + 关键公式/要点,一眼扫到,适合考前突击
|
||||
2. 🌳 **思维导图式** — 按层级结构组织(大标题→子标题→要点),有大纲感,适合梳理体系
|
||||
3. ❓ **Q&A 式** — 把知识点变成"问题→答案"对,适合自测复习
|
||||
|
||||
用户选择后进入步骤 3。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 3:LLM 提炼内容
|
||||
|
||||
根据用户选择的风格,构建不同的 prompt 让 LLM 从原文中提炼 cheatsheet 内容。
|
||||
|
||||
#### 风格 1:知识点速查卡
|
||||
|
||||
提炼规则:
|
||||
- 提取所有核心概念、定义、公式、关键数据
|
||||
- 每个知识点用 **术语:一句话解释** 的格式
|
||||
- 相关知识点分组,每组有小标题
|
||||
- 重要公式/代码片段原样保留
|
||||
- 砍掉所有举例、过渡句、背景铺垫
|
||||
|
||||
输出结构:
|
||||
```
|
||||
## [分组标题]
|
||||
- **术语A**:一句话定义
|
||||
- **术语B**:一句话定义
|
||||
- 📐 公式:`公式内容`
|
||||
|
||||
## [分组标题]
|
||||
...
|
||||
```
|
||||
|
||||
#### 风格 2:思维导图式
|
||||
|
||||
提炼规则:
|
||||
- 提取文档的层级结构(章→节→要点)
|
||||
- 每个节点用最简短的语言概括
|
||||
- 最多 3 级深度(再深就塞不进一页了)
|
||||
- 用缩进和符号表达层级关系
|
||||
|
||||
输出结构:
|
||||
```
|
||||
# 主题
|
||||
|
||||
## 一级分支
|
||||
├─ 二级要点
|
||||
│ ├─ 细节 1
|
||||
│ └─ 细节 2
|
||||
└─ 二级要点
|
||||
└─ 细节
|
||||
```
|
||||
|
||||
#### 风格 3:Q&A 式
|
||||
|
||||
提炼规则:
|
||||
- 把每个知识点转化成一个问题
|
||||
- 答案控制在 1-3 句话
|
||||
- 问题从基础到进阶排列
|
||||
- 易混淆的概念出辨析题
|
||||
|
||||
输出结构:
|
||||
```
|
||||
## [主题分组]
|
||||
|
||||
**Q:什么是 XXX?**
|
||||
A:一句话回答。
|
||||
|
||||
**Q:XXX 和 YYY 的区别?**
|
||||
A:简短对比。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 步骤 4:用户确认与调整
|
||||
|
||||
LLM 生成内容后,**先以文本形式展示给用户**,询问:
|
||||
|
||||
> "内容整理好了,你看看有没有要调整的?比如:
|
||||
> - 某些部分要加重点标记?
|
||||
> - 某些内容要删掉或补充?
|
||||
> - 排版上有什么偏好?(比如字号再小一点、分区颜色区分等)"
|
||||
|
||||
用户确认"可以"后,进入步骤 5。
|
||||
用户提出修改 → 调整内容 → 再次展示 → 等待确认。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 5:生成 PDF
|
||||
|
||||
调用 PDF skill 的 **Report 路线(ReportLab)** 生成双栏 PDF。
|
||||
|
||||
**排版规格(默认值):**
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| 页面大小 | A4 | 可按用户要求调整 |
|
||||
| 栏数 | 双栏 | 默认双栏,用户可选单栏 |
|
||||
| 正文字号 | 8pt | 信息密度优先,可按用户要求调整 |
|
||||
| 标题字号 | 10pt(一级)/ 9pt(二级) | |
|
||||
| 行距 | 1.2 | 紧凑但可读 |
|
||||
| 页边距 | 上下左右各 12mm | 最大化内容区域 |
|
||||
| 字体 | 中文用 UniSong/UniHei,英文用 Helvetica | |
|
||||
|
||||
**生成流程:**
|
||||
|
||||
1. 将 LLM 提炼的 Markdown 内容转换为 ReportLab 排版指令
|
||||
2. 调 PDF skill 的 report 路线生成 PDF
|
||||
3. 生成文件路径告知用户
|
||||
|
||||
**调用 PDF skill 时遵循其 SKILL.md 中的所有规则**,包括:
|
||||
- CJK 字体检查
|
||||
- 表格溢出防护
|
||||
- 页面填充率检查
|
||||
- 元数据设置
|
||||
|
||||
---
|
||||
|
||||
### 步骤 6:交付
|
||||
|
||||
输出给用户:
|
||||
- 📄 PDF 文件路径
|
||||
- 文件大小、页数
|
||||
- 提示用户可以继续调整
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 内容质量
|
||||
- **不编造内容**:所有知识点必须来自原文,不能自由发挥
|
||||
- **不遗漏关键内容**:核心概念、公式、定义必须保留
|
||||
- **术语保持原文用词**:不要擅自替换专业术语
|
||||
|
||||
### 文件操作
|
||||
- 生成的 PDF 默认保存到工作区根目录,文件名格式:`知识浓缩卡_[主题]_[日期].pdf`
|
||||
- 查重见上方"⚠️ 铁律:避免重复创建"
|
||||
|
||||
---
|
||||
|
||||
## 与其他 Skill 的关系
|
||||
|
||||
| Skill | 关系 |
|
||||
|-------|------|
|
||||
| PDF skill | 调用其 process 路线提取文本,调用其 report 路线生成 PDF |
|
||||
| study-buddy | study-buddy 可在用户完成学习项目后推荐生成 cheatsheet |
|
||||
| quiz-mastery | 无直接关系,但 cheatsheet 内容可作为出题的知识点来源 |
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
skills/cheat-sheet/
|
||||
├── SKILL.md ← 当前文件
|
||||
```
|
||||
|
||||
本 skill 是纯流程指引,不包含独立脚本。所有文件操作和 PDF 生成通过调用 PDF skill 完成。
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
---
|
||||
name: coding-agent
|
||||
slug: code
|
||||
version: 1.0.4
|
||||
homepage: https://clawic.com/skills/code
|
||||
description: Coding workflow with planning, implementation, verification, and testing for clean software development.
|
||||
changelog: Improved description for better discoverability
|
||||
metadata: {"clawdbot":{"emoji":"💻","requires":{"bins":[]},"os":["linux","darwin","win32"]}}
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
User explicitly requests code implementation. Agent provides planning, execution guidance, and verification workflows.
|
||||
|
||||
## Architecture
|
||||
|
||||
User preferences stored in `~/code/` when user explicitly requests.
|
||||
|
||||
```
|
||||
~/code/
|
||||
- memory.md # User-provided preferences only
|
||||
```
|
||||
|
||||
Create on first use: `mkdir -p ~/code`
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Topic | File |
|
||||
|-------|------|
|
||||
| Memory setup | `memory-template.md` |
|
||||
| Task breakdown | `planning.md` |
|
||||
| Execution flow | `execution.md` |
|
||||
| Verification | `verification.md` |
|
||||
| Multi-task state | `state.md` |
|
||||
| User criteria | `criteria.md` |
|
||||
|
||||
## Scope
|
||||
|
||||
This skill ONLY:
|
||||
- Provides coding workflow guidance
|
||||
- Stores preferences user explicitly provides in `~/code/`
|
||||
- Reads included reference files
|
||||
|
||||
This skill NEVER:
|
||||
- Executes code automatically
|
||||
- Makes network requests
|
||||
- Accesses files outside `~/code/` and the user's project
|
||||
- Modifies its own SKILL.md or auxiliary files
|
||||
- Takes autonomous action without user awareness
|
||||
|
||||
## Core Rules
|
||||
|
||||
### 1. Check Memory First
|
||||
Read `~/code/memory.md` for user's stated preferences if it exists.
|
||||
|
||||
### 2. User Controls Execution
|
||||
- This skill provides GUIDANCE, not autonomous execution
|
||||
- User decides when to proceed to next step
|
||||
- Sub-agent delegation requires user's explicit request
|
||||
|
||||
### 3. Plan Before Code
|
||||
- Break requests into testable steps
|
||||
- Each step independently verifiable
|
||||
- See `planning.md` for patterns
|
||||
|
||||
### 4. Verify Everything
|
||||
| After | Do |
|
||||
|-------|-----|
|
||||
| Each function | Suggest running tests |
|
||||
| UI changes | Suggest taking screenshot |
|
||||
| Before delivery | Suggest full test suite |
|
||||
|
||||
### 5. Store Preferences on Request
|
||||
| User says | Action |
|
||||
|-----------|--------|
|
||||
| "Remember I prefer X" | Add to memory.md |
|
||||
| "Never do Y again" | Add to memory.md Never section |
|
||||
|
||||
Only store what user explicitly asks to save.
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
Request -> Plan -> Execute -> Verify -> Deliver
|
||||
```
|
||||
|
||||
## Common Traps
|
||||
|
||||
- **Delivering untested code** -> always verify first
|
||||
- **Huge PRs** -> break into testable chunks
|
||||
- **Ignoring preferences** -> check memory.md first
|
||||
|
||||
## Self-Modification
|
||||
|
||||
This skill NEVER modifies its own SKILL.md or auxiliary files.
|
||||
User data stored only in `~/code/memory.md` after explicit request.
|
||||
|
||||
## External Endpoints
|
||||
|
||||
This skill makes NO network requests.
|
||||
|
||||
| Endpoint | Data Sent | Purpose |
|
||||
|----------|-----------|---------|
|
||||
| None | None | N/A |
|
||||
|
||||
## Security & Privacy
|
||||
|
||||
**Data that stays local:**
|
||||
- Only preferences user explicitly asks to save
|
||||
- Stored in `~/code/memory.md`
|
||||
|
||||
**Data that leaves your machine:**
|
||||
- None. This skill makes no network requests.
|
||||
|
||||
**This skill does NOT:**
|
||||
- Execute code automatically
|
||||
- Access network or external services
|
||||
- Access files outside `~/code/` and user's project
|
||||
- Take autonomous actions without user awareness
|
||||
- Delegate to sub-agents without user's explicit request
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn73vp5rarc3b14rc7wjcw8f8580t5d1",
|
||||
"slug": "code",
|
||||
"version": "1.0.4",
|
||||
"publishedAt": 1771467169291
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
# Criteria for Storing Preferences
|
||||
|
||||
Reference for when to save user preferences to `~/code/memory.md`.
|
||||
|
||||
## When to Save (User Must Request)
|
||||
|
||||
Save only when user explicitly asks:
|
||||
- "Remember that I prefer X"
|
||||
- "Always do Y from now on"
|
||||
- "Save this preference"
|
||||
- "Don't forget that I like Z"
|
||||
|
||||
## When NOT to Save
|
||||
|
||||
- User didn't explicitly ask to save
|
||||
- Project-specific requirement (applies to this project only)
|
||||
- One-off request ("just this once")
|
||||
- Temporary preference
|
||||
|
||||
## What to Save
|
||||
|
||||
**Preferences:**
|
||||
- Coding style preferences user stated
|
||||
- Tools or frameworks user prefers
|
||||
- Patterns user explicitly likes
|
||||
|
||||
**Things to avoid:**
|
||||
- Approaches user explicitly dislikes
|
||||
- Patterns user asked not to repeat
|
||||
|
||||
## Format in memory.md
|
||||
|
||||
```markdown
|
||||
## Preferences
|
||||
- prefers TypeScript over JavaScript
|
||||
- likes detailed comments
|
||||
- wants tests for all functions
|
||||
|
||||
## Never
|
||||
- no class-based React components
|
||||
- avoid inline styles
|
||||
```
|
||||
|
||||
## Important
|
||||
|
||||
- Only save what user EXPLICITLY asked to save
|
||||
- Ask user before saving: "Should I remember this preference?"
|
||||
- Never modify any skill files, only `~/code/memory.md`
|
||||
Executable
+42
@@ -0,0 +1,42 @@
|
||||
# Execution Guidance
|
||||
|
||||
Reference for executing multi-step implementations.
|
||||
|
||||
## Recommended Flow
|
||||
|
||||
When user approves a step:
|
||||
1. Execute that step
|
||||
2. Verify it works
|
||||
3. Report completion to user
|
||||
4. Wait for user to approve next step
|
||||
|
||||
## Progress Tracking
|
||||
|
||||
Show user the current state:
|
||||
```
|
||||
- [DONE] Step 1 (completed)
|
||||
- [WIP] Step 2 <- awaiting user approval
|
||||
- [ ] Step 3
|
||||
- [ ] Step 4
|
||||
```
|
||||
|
||||
## When to Pause and Ask User
|
||||
|
||||
- Before starting any new step
|
||||
- When encountering an error
|
||||
- When a decision is needed (A vs B)
|
||||
- When credentials or permissions are needed
|
||||
|
||||
## Error Handling
|
||||
|
||||
If an error occurs:
|
||||
1. Report the error to user
|
||||
2. Suggest possible fixes
|
||||
3. Wait for user decision on how to proceed
|
||||
|
||||
## Patterns to Follow
|
||||
|
||||
- Report completion of each step
|
||||
- Ask before proceeding to next step
|
||||
- Let user decide retry strategy
|
||||
- Keep user informed of progress
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
# Memory Setup - Code
|
||||
|
||||
## Initial Setup
|
||||
|
||||
Create directory on first use:
|
||||
```bash
|
||||
mkdir -p ~/code
|
||||
touch ~/code/memory.md
|
||||
```
|
||||
|
||||
## memory.md Template
|
||||
|
||||
Copy to `~/code/memory.md`:
|
||||
|
||||
```markdown
|
||||
# Code Memory
|
||||
|
||||
## Preferences
|
||||
<!-- User's coding workflow preferences. Format: "preference" -->
|
||||
<!-- Examples: always run tests, prefer TypeScript, commit after each feature -->
|
||||
|
||||
## Never
|
||||
<!-- Things that don't work for this user. Format: "thing to avoid" -->
|
||||
<!-- Examples: inline styles, console.log debugging, large PRs -->
|
||||
|
||||
## Patterns
|
||||
<!-- Approaches that work well. Format: "pattern: context" -->
|
||||
<!-- Examples: TDD: for complex logic, screenshots: for UI work -->
|
||||
|
||||
---
|
||||
Last updated: YYYY-MM-DD
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Check `criteria.md` for additional user-specific criteria
|
||||
- Use `planning.md` for breaking down complex requests
|
||||
- Verify with tests and screenshots per `verification.md`
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
# Planning Reference
|
||||
|
||||
Consult when breaking down a multi-step request.
|
||||
|
||||
## When to Plan
|
||||
- Multiple files or components
|
||||
- Dependencies between parts
|
||||
- UI that needs visual verification
|
||||
- User says "build", "create", "implement"
|
||||
|
||||
## Step Format
|
||||
```
|
||||
Step N: [What]
|
||||
- Output: [What exists after]
|
||||
- Test: [How to verify]
|
||||
```
|
||||
|
||||
## Good Steps
|
||||
- Clear output (file, endpoint, screen)
|
||||
- Testable independently
|
||||
- No ambiguity in what "done" means
|
||||
|
||||
## Bad Steps
|
||||
- "Implement the thing" (vague output)
|
||||
- No test defined
|
||||
- Depends on undefined prior step
|
||||
|
||||
## Don't Plan
|
||||
- One-liner functions
|
||||
- Simple modifications
|
||||
- Questions about existing code
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
# State Tracking Guidance
|
||||
|
||||
Reference for tracking multiple tasks or requests.
|
||||
|
||||
## Request Tracking
|
||||
|
||||
Label each user request:
|
||||
```
|
||||
[R1] Build login page
|
||||
[R2] Add dark mode
|
||||
[R3] Fix header alignment
|
||||
```
|
||||
|
||||
Track state for user visibility:
|
||||
```
|
||||
[R1] [DONE] Done
|
||||
[R2] [WIP] In progress (awaiting user approval for step 2)
|
||||
[R3] [Q] Queued
|
||||
```
|
||||
|
||||
## Managing Multiple Requests
|
||||
|
||||
When user sends a new request while another is in progress:
|
||||
|
||||
1. Acknowledge: "Got it, I'll add this to the queue"
|
||||
2. Show updated queue to user
|
||||
3. Ask user if priority should change
|
||||
|
||||
## Handling Interruptions
|
||||
|
||||
| Situation | Suggested Action |
|
||||
|-----------|------------------|
|
||||
| New unrelated request | Add to queue, ask user priority |
|
||||
| Request affects current work | Pause, explain impact, ask user how to proceed |
|
||||
| User says "stop" or "wait" | Stop immediately, await instructions |
|
||||
| User changes requirements | Summarize impact, ask user to confirm changes |
|
||||
|
||||
## User Decisions
|
||||
|
||||
Always ask user before:
|
||||
- Starting work on queued items
|
||||
- Changing priority order
|
||||
- Rolling back completed work
|
||||
- Modifying the plan
|
||||
|
||||
## Progress File (Optional)
|
||||
|
||||
User may request a state file:
|
||||
```markdown
|
||||
## In Progress
|
||||
[R2] Dark mode - Step 2/4 (awaiting user approval)
|
||||
|
||||
## Queued
|
||||
[R3] Header fix
|
||||
|
||||
## Done
|
||||
[R1] Login page [DONE]
|
||||
```
|
||||
|
||||
Update only when user requests or approves changes.
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
# Verification Reference
|
||||
|
||||
Consult when verifying implementations visually or with tests.
|
||||
|
||||
## Screenshots
|
||||
- Wait for full page load (no spinners)
|
||||
- Review yourself before sending
|
||||
- Split long pages into 3-5 sections (~800px each)
|
||||
- Caption each: "Hero", "Features", "Footer"
|
||||
|
||||
## Before Sending
|
||||
```
|
||||
[ ] Content loaded
|
||||
[ ] Shows the specific change
|
||||
[ ] No visual bugs
|
||||
[ ] Caption explains what user sees
|
||||
```
|
||||
|
||||
## Fix-Before-Send
|
||||
If screenshot shows problem:
|
||||
1. Fix code
|
||||
2. Re-deploy
|
||||
3. New screenshot
|
||||
4. Still broken? -> back to 1
|
||||
5. Fixed? -> now send
|
||||
|
||||
Never send "I noticed X is wrong, will fix" - fix first.
|
||||
|
||||
## No UI? Show Output
|
||||
|
||||
When verifying API endpoints, show actual output:
|
||||
```
|
||||
GET /api/users -> {"id": 1, "name": "test"}
|
||||
```
|
||||
|
||||
Include actual response, not just "it works".
|
||||
|
||||
## Flows
|
||||
Number sequential states: "1/4: Form", "2/4: Loading", "3/4: Error", "4/4: Success"
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
---
|
||||
name: content-strategy
|
||||
description: Build and execute a content marketing strategy for a solopreneur business. Use when planning what content to create, deciding on content formats and channels, building a content calendar, measuring content performance, or systematizing content production. Covers audience research for content, content pillars, distribution strategy, repurposing workflows, and metrics. Trigger on "content strategy", "content marketing", "what content should I create", "content plan", "content calendar", "content ideas", "content distribution", "grow through content".
|
||||
---
|
||||
|
||||
# Content Strategy
|
||||
|
||||
## Overview
|
||||
Content marketing is how solopreneurs build authority, attract customers, and grow without paid ads. But random content doesn't work — you need a strategy. This playbook builds a repeatable system for creating content that actually drives business results, not just likes.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Define Your Content Goals
|
||||
|
||||
Content without a goal is just noise. Before you create anything, answer: what is this content supposed to DO?
|
||||
|
||||
**Common solopreneur content goals:**
|
||||
- **Generate awareness** (new people discover you exist)
|
||||
- **Build trust** (people see you as credible and knowledgeable)
|
||||
- **Drive leads** (people give you their email or book a call)
|
||||
- **Enable sales** (content answers objections and shortens sales cycles)
|
||||
- **Retain customers** (existing customers stay engaged and see ongoing value)
|
||||
|
||||
**Rule:** Pick ONE primary goal per piece of content. You can have secondary benefits, but clarity on the main goal determines format, channel, and CTA.
|
||||
|
||||
Example: A tutorial blog post might have the primary goal of "generate awareness" (via SEO) and a secondary goal of "drive leads" (with an email signup CTA at the end).
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Research Your Audience's Content Needs
|
||||
|
||||
Great content solves a specific problem for a specific person. Bad content talks about what YOU want to talk about.
|
||||
|
||||
**Research workflow (spend 2-3 hours on this before creating anything):**
|
||||
|
||||
1. **Mine customer conversations.** Go through support tickets, sales calls, discovery calls. What questions do prospects and customers ask repeatedly? Those are your content topics.
|
||||
|
||||
2. **Check competitor content.** What are the top 3-5 players in your space publishing? Look for gaps — topics they're NOT covering or covering poorly.
|
||||
|
||||
3. **Keyword research (if doing SEO).** Use free tools (Google autocomplete, AnswerThePublic, or "People Also Ask" in Google results) to see what people are actually searching for related to your niche.
|
||||
|
||||
4. **Community mining.** Go to Reddit, Slack communities, Facebook groups, or forums in your space. What questions get asked over and over? Those are high-value topics.
|
||||
|
||||
**Output:** A list of 20-30 content ideas ranked by: (a) relevance to your ICP, (b) search volume or community demand, (c) your unique perspective or experience on the topic.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Build Content Pillars
|
||||
|
||||
Content pillars are 3-5 broad topic areas that all your content falls under. They keep you focused and prevent random one-off content that doesn't build momentum.
|
||||
|
||||
**How to define pillars:**
|
||||
- Each pillar should map to a core problem your product/service solves or a key interest area of your ICP.
|
||||
- Pillars should be broad enough to generate dozens of pieces of content but specific enough to be relevant.
|
||||
- Aim for 3-5 pillars max. More than that dilutes focus.
|
||||
|
||||
**Example (for an n8n automation consultant):**
|
||||
```
|
||||
Pillar 1: Workflow Automation Fundamentals
|
||||
Pillar 2: No-Code Tool Comparisons
|
||||
Pillar 3: Business Process Optimization
|
||||
Pillar 4: Real Client Case Studies
|
||||
```
|
||||
|
||||
Every piece of content you create should fit under one of these pillars. If it doesn't, don't create it.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Choose Your Content Formats and Channels
|
||||
|
||||
Solopreneurs can't do everything. Pick 1-2 primary formats and 1-2 primary channels. Go deep, not wide.
|
||||
|
||||
**Content formats:**
|
||||
| Format | Best For | Time Investment | Longevity |
|
||||
|---|---|---|---|
|
||||
| **Blog posts** | SEO, teaching, depth | 2-4 hrs/post | High (evergreen) |
|
||||
| **Videos (YouTube)** | Visual topics, personality-driven brands | 3-6 hrs/video | High (evergreen) |
|
||||
| **Podcasts** | Thought leadership, interviews | 2-3 hrs/episode | Medium |
|
||||
| **Twitter/X threads** | Quick insights, community building | 30 min/thread | Low (24-48hr shelf life) |
|
||||
| **LinkedIn posts** | B2B, professional content | 30-60 min/post | Low-medium |
|
||||
| **Email newsletters** | Relationship building, owned audience | 1-2 hrs/newsletter | Medium (subscribers keep it) |
|
||||
| **Short-form video (TikTok, Reels)** | Viral potential, younger demos | 1-2 hrs/video | Low (algorithmic churn) |
|
||||
|
||||
**Selection criteria:**
|
||||
- Where does your ICP hang out? (B2B = LinkedIn. Developers = Twitter. Visual products = Instagram.)
|
||||
- What format do you NOT hate creating? (If you hate being on camera, don't pick YouTube.)
|
||||
- What has the best ROI for your goals? (Lead gen = blog + email. Brand building = Twitter + LinkedIn.)
|
||||
|
||||
**Recommended solopreneur starting stack:**
|
||||
- **Primary format:** Blog posts or long-form LinkedIn posts (depending on B2B vs B2C)
|
||||
- **Secondary format:** Email newsletter (this is your owned channel — never skip this)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Build a Content Calendar
|
||||
|
||||
A content calendar prevents the "what should I post today?" panic. Plan 2-4 weeks ahead.
|
||||
|
||||
**Calendar structure:**
|
||||
```
|
||||
DATE | PILLAR | TOPIC | FORMAT | CHANNEL | CTA | STATUS
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Feb 10 | Automation | "5 n8n workflows every SaaS founder needs" | Blog | Website + LinkedIn | Email signup | Draft
|
||||
Feb 13 | Case Study | "How we saved Client X 20hrs/week" | LinkedIn post | LinkedIn | Book a call | Scheduled
|
||||
Feb 17 | Tool Comparison | "Zapier vs n8n: Which is right for you?" | Blog | Website + Twitter | Free guide download | Outline
|
||||
```
|
||||
|
||||
**Cadence recommendations:**
|
||||
- Blog: 1-2x/week (minimum 2x/month to maintain SEO momentum)
|
||||
- Newsletter: 1x/week or biweekly (consistency matters more than frequency)
|
||||
- Social (LinkedIn/Twitter): 3-5x/week
|
||||
|
||||
**Rule:** Batch creation. Write 4 posts in one sitting rather than 1 post four different days. Batching is 3x faster and produces better quality.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Distribution and Amplification
|
||||
|
||||
Creating content is 30% of the work. Distribution is the other 70%.
|
||||
|
||||
**Distribution checklist for every piece:**
|
||||
- [ ] Publish on primary channel (blog, YouTube, etc.)
|
||||
- [ ] Share on 2-3 social channels with unique captions per platform (don't just copy-paste the same message)
|
||||
- [ ] Send to email list (if it's a high-value piece)
|
||||
- [ ] Post in 1-2 relevant communities (but add value to the discussion, don't just drop links)
|
||||
- [ ] DM it to 3-5 people who you think would find it genuinely useful
|
||||
- [ ] Repurpose into 2-3 other formats (see next step)
|
||||
|
||||
**Timing:** Publish early in the week (Tuesday-Thursday) for best engagement. Avoid Fridays and weekends unless your audience is specifically active then.
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Repurpose Everything
|
||||
|
||||
One piece of long-form content can become 5-10 smaller pieces. This is how solopreneurs produce high volume without burning out.
|
||||
|
||||
**Repurposing workflow (example: one blog post):**
|
||||
1. Original: 1,500-word blog post
|
||||
2. Repurpose into: LinkedIn post (first 3 paragraphs + a hook)
|
||||
3. Repurpose into: Twitter thread (key points broken into 8-10 tweets)
|
||||
4. Repurpose into: Email newsletter (add a personal intro, link to full post)
|
||||
5. Repurpose into: Carousel post (main points as slides on LinkedIn or Instagram)
|
||||
6. Repurpose into: Short video (you on camera summarizing the key takeaway in 60 seconds)
|
||||
|
||||
**Rule:** Repurpose the high-performers. If a blog post gets good traffic or a LinkedIn post gets strong engagement, milk it — turn it into 5 more formats.
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Measure What Matters
|
||||
|
||||
Track content performance so you can double down on what works and stop doing what doesn't.
|
||||
|
||||
**Metrics by goal:**
|
||||
|
||||
| Goal | Metrics to Track |
|
||||
|---|---|
|
||||
| Awareness | Impressions, reach, new visitors, social followers |
|
||||
| Trust | Engagement rate (comments, shares), time on page, repeat visitors |
|
||||
| Lead generation | Email signups, CTA clicks, lead magnet downloads |
|
||||
| Sales enablement | Content assists (how many deals involved this content?), proposal open rates (if content is attached) |
|
||||
|
||||
**Dashboard (monthly check-in):**
|
||||
- Top 5 performing pieces (by traffic or engagement)
|
||||
- Traffic source breakdown (organic, social, direct, referral)
|
||||
- Conversion rate (visitors → email signups or leads)
|
||||
- Time investment vs results (which content type has the best ROI?)
|
||||
|
||||
**Iteration rule:** Every month, identify the top-performing content type and topic. Do 2x more of that next month. Identify the worst performer. Stop doing that format or adjust the approach.
|
||||
|
||||
---
|
||||
|
||||
## Content Strategy Mistakes to Avoid
|
||||
- Creating content without a goal. Every piece should have a purpose tied to a business outcome.
|
||||
- Not researching what your audience actually wants. Your assumptions are often wrong — validate with real data.
|
||||
- Trying to be on every platform. Pick 1-2 and dominate them before expanding.
|
||||
- Publishing inconsistently. One post a month doesn't build momentum. Consistency compounds.
|
||||
- Not repurposing. Creating 10 original pieces is 5x harder than creating 2 original pieces and repurposing them into 8 more.
|
||||
- Ignoring metrics. If you don't measure, you can't improve. Check your numbers monthly at minimum.
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn732qfbv22he1jqm63xbwq6e980kn8s",
|
||||
"slug": "content-strategy",
|
||||
"version": "0.1.0",
|
||||
"publishedAt": 1770341804646
|
||||
}
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
---
|
||||
name: ExtractWisdom
|
||||
description: Content-adaptive wisdom extraction — detects what domains exist in content and builds custom sections (not static IDEAS/QUOTES). Produces tailored insight reports from videos, podcasts, articles. USE WHEN extract wisdom, analyze video, analyze podcast, extract insights, what's interesting, extract from YouTube, what did I miss, key takeaways.
|
||||
---
|
||||
|
||||
## Customization
|
||||
|
||||
**Before executing, check for user customizations at:**
|
||||
`~/.claude/PAI/USER/SKILLCUSTOMIZATIONS/ExtractWisdom/`
|
||||
|
||||
If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults.
|
||||
|
||||
# ExtractWisdom — Dynamic Content Extraction
|
||||
|
||||
**The next generation of extract_wisdom.** Instead of static sections (IDEAS, QUOTES, HABITS...), this skill detects what wisdom domains actually exist in the content and builds custom sections around them.
|
||||
|
||||
A programming interview gets "Programming Philosophy" and "Developer Workflow Tips." A business podcast gets "Contrarian Business Takes" and "Money Philosophy." A security talk gets "Threat Model Insights" and "Defense Strategies." The sections adapt because the content dictates them.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Analyzing YouTube videos, podcasts, interviews, articles
|
||||
- User says "extract wisdom", "what's interesting in this", "key takeaways"
|
||||
- Processing any content where you want to capture the best stuff
|
||||
- When standard extraction patterns miss the gems
|
||||
|
||||
## Depth Levels
|
||||
|
||||
Extract at different depths depending on need. Default is **Full** if no level is specified.
|
||||
|
||||
| Level | Sections | Bullets/Section | Closing Sections | When |
|
||||
|-------|----------|----------------|-----------------|------|
|
||||
| **Instant** | 1 | 8 | None | Quick hit. One killer section. |
|
||||
| **Fast** | 3 | 3 | None | Skim in 30 seconds. |
|
||||
| **Basic** | 3 | 5 | One-Sentence Takeaway only | Solid overview without the deep cuts. |
|
||||
| **Full** | 5-12 | 3-15 | All three | The default. Complete extraction. |
|
||||
| **Comprehensive** | 10-15 | 8-15 | All three + Themes & Connections | Maximum depth. Nothing left behind. |
|
||||
|
||||
**How to invoke:** "extract wisdom (fast)" or "extract wisdom at comprehensive level" or just "extract wisdom" for Full.
|
||||
|
||||
**Comprehensive extras:**
|
||||
- **Themes & Connections** closing section: identify 3-5 throughlines that connect multiple sections. Not summaries — the deeper patterns the speaker may not even realize they're revealing.
|
||||
- Prioritize breadth. Every significant wisdom domain gets its own section.
|
||||
- No merging sections to save space. If the content supports 15 sections, use 15.
|
||||
|
||||
**All levels use the same voice, tone rules, and quality standards.** The only thing that changes is structure. An Instant extraction should hit just as hard per-bullet as a Comprehensive one.
|
||||
|
||||
## Workflow Routing
|
||||
|
||||
| Workflow | Trigger | File |
|
||||
|----------|---------|------|
|
||||
| **Extract** | "extract wisdom from", "analyze this", YouTube URL | `Workflows/Extract.md` |
|
||||
|
||||
## The Core Idea
|
||||
|
||||
Old extract_wisdom: Static sections. Same headers every time. IDEAS. QUOTES. HABITS. FACTS.
|
||||
|
||||
This skill: **Read the content first. Figure out what's actually in there. Build sections around what you find.**
|
||||
|
||||
The output should feel like your smartest friend watched/read the thing and is telling you about it over coffee. Not a book report. Not documentation. A real person pointing out the parts that made them go "holy shit" or "wait, that's actually brilliant."
|
||||
|
||||
## Tone Rules (CRITICAL)
|
||||
|
||||
**Canonical voice reference: `PAI/USER/WRITINGSTYLE.md`** — read this file for the full voice definition. The bullets should sound like {PRINCIPAL.NAME} telling a friend about it over coffee. Not compressed info nuggets. Not clever one-liners. Actual spoken observations.
|
||||
|
||||
**THREE LEVELS — we're aiming for Level 3:**
|
||||
|
||||
**Level 1 (BAD — documentation):**
|
||||
- The speaker discussed the importance of self-modifying software in the context of agentic AI development
|
||||
- It was noted that financial success has diminishing returns beyond a certain threshold
|
||||
- The distinction between "vibe coding" and "agentic engineering" was emphasized as meaningful
|
||||
|
||||
**Level 2 (BETTER — but still "smart bullet points"):**
|
||||
- He built self-modifying software basically by accident — just made the agent aware of its own source code
|
||||
- Money has diminishing returns. A cheeseburger is a cheeseburger no matter how rich you are.
|
||||
- "Vibe coding is a slur" — he calls it agentic engineering, and only does vibe coding after 3am
|
||||
|
||||
**Level 3 (YES — this is what we want — conversational, {PRINCIPAL.NAME}'s voice):**
|
||||
- He wasn't trying to build self-modifying software. He just let the agent see its own source code and it started fixing itself.
|
||||
- Past a certain point, money stops mattering. A cheeseburger is a cheeseburger no matter how rich you are.
|
||||
- He calls vibe coding a slur. What he does is agentic engineering. The vibe coding only happens after 3am, and he regrets it in the morning.
|
||||
|
||||
**The difference between Level 2 and 3:** Level 2 is compressed info with em-dashes. Level 3 is how you'd actually SAY it. Varied sentence lengths. Letting a thought breathe. Not trying to be clever — just being clear and direct and a little bit personal.
|
||||
|
||||
**Key signals of Level 3:**
|
||||
- Reads naturally when spoken aloud
|
||||
- Varied sentence lengths — some short, some longer
|
||||
- Understated — lets the content carry the weight
|
||||
- Uses periods, not em-dashes, to let ideas land
|
||||
- Feels opinionated ("Past a certain point, money stops mattering") not just informational
|
||||
- The reader should think "I want to watch this" not "I got the summary"
|
||||
|
||||
## Rules for Extracted Points
|
||||
|
||||
1. **Write like you'd say it.** Read each bullet aloud. If it sounds like a press release or a compressed tweet, rewrite it. If it sounds like you telling a friend what you just watched, you nailed it.
|
||||
2. **8-16 words per sentence.** This is the target range. Mix short (8-10) with medium (11-14) and longer (15-16). Don't make them all the same length. Exception: verbatim quotes can be any length since they're the speaker's actual words.
|
||||
3. **Let ideas breathe.** Use periods between thoughts, not em-dashes. Short sentences. Then a slightly longer one to explain. That's the rhythm.
|
||||
4. **Include the actual detail.** Not "he talked about money" but "a cheeseburger is a cheeseburger no matter how rich you are."
|
||||
5. **Use the speaker's words when they're good.** If they said something perfectly, use it.
|
||||
6. **No hedging language.** Not "it was suggested that" or "the speaker noted." Just say the thing.
|
||||
7. **Capture what made you stop.** Every bullet should be something worth telling someone about.
|
||||
8. **Vary your openers.** Don't start three bullets the same way. And don't front-load with "He" — if more than 3 bullets in a section start with the speaker's name, you're writing a biography.
|
||||
9. **Capture the human moments.** Burnout stories, moments of doubt, something that moved them. That's wisdom too. Don't skip it because it's not "technical."
|
||||
10. **Insight over inventory.** "He uses Go for CLIs" is inventory. "He picked a language he doesn't even like because the ecosystem fits agents perfectly. That's the new normal." is insight. Go deeper.
|
||||
11. **Specificity is everything.** "He was impressed by the agent" = bad. "The agent found ffmpeg, curled the Whisper API, and transcribed a voice message nobody taught it to handle" = good.
|
||||
12. **Tension and surprise.** The best bullets have a contradiction or reversal. "Every VC is offering hundreds of millions. He genuinely doesn't care." The gap between the offer and the indifference IS the wisdom.
|
||||
13. **Understated, not clever.** Let the content carry the weight. You don't need to manufacture drama or craft the perfect one-liner. Just state what's interesting plainly and move on.
|
||||
|
||||
## How Dynamic Sections Work
|
||||
|
||||
### Phase 1: Content Scan
|
||||
|
||||
Read/listen to the full content. As you go, notice what DOMAINS of wisdom are present. These aren't the topics discussed — they're the TYPES of insight being delivered.
|
||||
|
||||
Examples of wisdom domains (these are illustrative, not exhaustive):
|
||||
- Programming Philosophy (how to think about code, not specific syntax)
|
||||
- Developer Workflow (practical tips for how to work)
|
||||
- Business/Money Philosophy (unconventional takes on money, success, building companies)
|
||||
- Human Psychology (insights about how people think, behave, learn)
|
||||
- Technology Predictions (where things are headed)
|
||||
- Life Philosophy (how to live, what matters)
|
||||
- Contrarian Takes (things that go against conventional wisdom)
|
||||
- First-Time Revelations (things you're hearing for the first time — genuinely new)
|
||||
- Technical Architecture (how something is built, design decisions)
|
||||
- Leadership & Team Dynamics (managing people, working with others)
|
||||
- Creative Process (how to make things, craft, art)
|
||||
|
||||
### Phase 2: Section Selection
|
||||
|
||||
Pick sections based on depth level (default Full = 5-12). Requirements:
|
||||
- Section count follows depth level table. Full = 5-12, Comprehensive = 10-15, Basic/Fast = 3, Instant = 1.
|
||||
- Each section must have at least 3 STRONG bullets to justify existing (except Fast, where 3 tight bullets IS the section). If you can only scrape together 2 weak ones, merge into a related section.
|
||||
- Always include "Quotes That Hit Different" if the content has good ones
|
||||
- Always include "First-Time Revelations" if there are genuinely new ideas — things you literally didn't know before
|
||||
- Section names should be conversational, not academic. "Money Philosophy" not "Financial Considerations"
|
||||
- Sections should be SPECIFIC to this content. Generic sections = failure.
|
||||
- **Kill inventory sections.** If a section is just a list of facts ("uses X for Y, uses A for B"), it's not wisdom. Either go deeper on WHY those choices matter or merge the facts into a section about the underlying philosophy.
|
||||
- **Don't split what belongs together.** If "burnout recovery" and "money philosophy" are actually both about "what success really means," make one richer section instead of two thin ones.
|
||||
- **Name sections like a magazine editor.** "The Death of 80% of Apps" is great. "Technology Predictions" is not. The section name itself should make you curious. It's a headline, not a category.
|
||||
- **Surprise density per section.** If a section has 6+ bullets but only 2 are genuinely surprising, kill the padding and keep the winners. Quality > quantity per section.
|
||||
- **Don't drop your best material between drafts.** If a spicy take, stunning moment, or first-time revelation was identified in an earlier pass, it MUST survive into the final version. Losing great material is worse than adding mediocre material.
|
||||
|
||||
### Phase 3: Extraction
|
||||
|
||||
For each section, extract 3-15 bullets depending on density. Apply all tone rules. Every bullet earns its place.
|
||||
|
||||
**The Spiciest Take Rule:** If the speaker has a genuinely contrarian or hot take on a topic (e.g., "screw MCPs", "X is dead", "Y is overhyped"), that take MUST appear somewhere. Spicy takes are the most memorable, shareable, and valuable parts of any content. Don't water them down. Don't leave them out.
|
||||
|
||||
**The "Would I Tweet This?" Test:** After extraction, scan your bullets. If fewer than half would make a good standalone tweet or social media post, your bullets are too generic. The best extractions are effectively a thread of tweetable insights.
|
||||
|
||||
### Phase 4: Closing Sections (Depth-Level Dependent)
|
||||
|
||||
Which closing sections to include depends on depth level:
|
||||
|
||||
| Level | Closing Sections |
|
||||
|-------|-----------------|
|
||||
| **Instant** | None |
|
||||
| **Fast** | None |
|
||||
| **Basic** | One-Sentence Takeaway only |
|
||||
| **Full** | One-Sentence Takeaway + If You Only Have 2 Minutes + References & Rabbit Holes |
|
||||
| **Comprehensive** | All three above + Themes & Connections |
|
||||
|
||||
**One-Sentence Takeaway**
|
||||
The single most important thing from the entire piece in 15-20 words.
|
||||
|
||||
**If You Only Have 2 Minutes**
|
||||
The 5-7 absolute must-know points. The cream of the cream.
|
||||
|
||||
**References & Rabbit Holes**
|
||||
People, projects, books, tools, and ideas mentioned that are worth following up on. Brief context for each.
|
||||
|
||||
**Themes & Connections** (Comprehensive only)
|
||||
3-5 throughlines that connect multiple sections. The deeper patterns the speaker may not realize they're revealing. Not summaries. Synthesis.
|
||||
|
||||
## Output Format
|
||||
|
||||
```markdown
|
||||
# EXTRACT WISDOM: {Content Title}
|
||||
> {One-line description of what this is and who's talking}
|
||||
|
||||
---
|
||||
|
||||
## {Dynamic Section 1 Name}
|
||||
|
||||
- {bullet}
|
||||
- {bullet}
|
||||
- {bullet}
|
||||
|
||||
## {Dynamic Section 2 Name}
|
||||
|
||||
- {bullet}
|
||||
- {bullet}
|
||||
|
||||
[... more dynamic sections ...]
|
||||
|
||||
---
|
||||
|
||||
## One-Sentence Takeaway
|
||||
|
||||
{15-20 word sentence}
|
||||
|
||||
## If You Only Have 2 Minutes
|
||||
|
||||
- {essential point 1}
|
||||
- {essential point 2}
|
||||
- {essential point 3}
|
||||
- {essential point 4}
|
||||
- {essential point 5}
|
||||
|
||||
## References & Rabbit Holes
|
||||
|
||||
- **{Name/Project}** — {one-line context of why it's worth looking into}
|
||||
- **{Name/Project}** — {context}
|
||||
```
|
||||
|
||||
## Quality Check
|
||||
|
||||
Before delivering output, verify:
|
||||
- [ ] Sections are specific to THIS content, not generic
|
||||
- [ ] No bullet sounds like it was written by a committee
|
||||
- [ ] Every bullet has a specific detail, quote, or insight — not vague summaries
|
||||
- [ ] Section names are conversational and headline-worthy (not category labels)
|
||||
- [ ] Section count matches depth level (Instant=1, Fast/Basic=3, Full=5-12, Comprehensive=10-15)
|
||||
- [ ] Closing sections match depth level (see Phase 4 table)
|
||||
- [ ] No bullet starts with "The speaker" or "It was noted that"
|
||||
- [ ] No more than 3 bullets per section start with "He" or the speaker's name
|
||||
- [ ] No bullet exceeds 25 words
|
||||
- [ ] No inventory sections (just listing facts without insight)
|
||||
- [ ] "If You Only Have 2 Minutes" bullets are each under 20 words
|
||||
- [ ] Reading the output makes you want to consume the original content
|
||||
@@ -0,0 +1,60 @@
|
||||
# Extract Workflow
|
||||
|
||||
Extract dynamic, content-adaptive wisdom from any content source.
|
||||
|
||||
## Input Sources
|
||||
|
||||
| Source | Method |
|
||||
|--------|--------|
|
||||
| YouTube URL | `fabric -y "URL"` to get transcript |
|
||||
| Article URL | WebFetch to get content |
|
||||
| File path | Read the file directly |
|
||||
| Pasted text | Use directly |
|
||||
|
||||
## Execution Steps
|
||||
|
||||
### Step 1: Get the Content
|
||||
|
||||
Obtain the full text/transcript. For YouTube, use `fabric -y "URL"` to extract transcript. Save to a working file if large.
|
||||
|
||||
### Step 2: Deep Read
|
||||
|
||||
Read the entire content. Don't extract yet. Notice:
|
||||
- What domains of wisdom are present?
|
||||
- What made you stop and think?
|
||||
- What's genuinely novel vs. commonly known?
|
||||
- What would {PRINCIPAL.NAME} highlight if he were reading this?
|
||||
- What quotes land perfectly?
|
||||
|
||||
### Step 3: Select Dynamic Sections
|
||||
|
||||
Based on your deep read, pick 5-12 section names. Rules:
|
||||
- Section names must be conversational, not academic
|
||||
- Each must have at least 3 quality bullets
|
||||
- Always include "Quotes That Hit Different" if source has quotable moments
|
||||
- Always include "First-Time Revelations" if genuinely new ideas exist
|
||||
- Be SPECIFIC — "Agentic Engineering Philosophy" not "Technology Insights"
|
||||
|
||||
### Step 4: Extract Per Section
|
||||
|
||||
For each section, extract 3-15 bullets. Apply tone rules from SKILL.md:
|
||||
- 8-20 words, flexible for clarity
|
||||
- Specific details, not vague summaries
|
||||
- Speaker's words when they're good
|
||||
- No hedging language
|
||||
- Every bullet worth telling someone about
|
||||
|
||||
### Step 5: Add Closing Sections
|
||||
|
||||
Always append:
|
||||
1. **One-Sentence Takeaway** (15-20 words)
|
||||
2. **If You Only Have 2 Minutes** (5-7 essential points)
|
||||
3. **References & Rabbit Holes** (people, projects, books, tools mentioned)
|
||||
|
||||
### Step 6: Quality Check
|
||||
|
||||
Run the quality checklist from SKILL.md before delivering.
|
||||
|
||||
### Step 7: Output
|
||||
|
||||
Present the complete extraction in the format specified in SKILL.md.
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: ContentAnalysis
|
||||
description: Content extraction and analysis — wisdom extraction from videos, podcasts, articles, and YouTube. USE WHEN extract wisdom, content analysis, analyze content, insight report, analyze video, analyze podcast, extract insights, key takeaways, what did I miss, extract from YouTube.
|
||||
---
|
||||
|
||||
# ContentAnalysis
|
||||
|
||||
Unified skill for content extraction and analysis workflows.
|
||||
|
||||
## Workflow Routing
|
||||
|
||||
| Request Pattern | Route To |
|
||||
|---|---|
|
||||
| Extract wisdom, content analysis, insight report, analyze content | `ExtractWisdom/SKILL.md` |
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
# Design Skill Index
|
||||
|
||||
This file is a directory map for the design skill package. It should not contain runtime routing rules. Runtime behavior lives in `SKILL.md`; template selection lives in `design-templates/TEMPLATE-ROUTER.md`.
|
||||
|
||||
## Runtime Entry
|
||||
|
||||
### `SKILL.md`
|
||||
|
||||
Main design router. It decides the primary artifact skill, output target, design-system/reference usage, template usage, horizontal craft triggers, quality gate, export, and version-management handoff.
|
||||
|
||||
## Active Artifact Skills
|
||||
|
||||
| File | Output target | Use for |
|
||||
|---|---|---|
|
||||
| `landing-page.md` | `responsive-html` | Product landing pages, SaaS homepages, marketing pages, waitlists, service pages. |
|
||||
| `portfolio.md` | `responsive-html` | Personal sites, portfolios, creator profiles, resumes, project showcases. |
|
||||
| `prototype.md` | `responsive-html` / `mobile-html` | Product prototypes, app flows, dashboards, admin panels, wireframes, UI mockups. |
|
||||
| `content-page.md` | `responsive-html` | Articles, editorial pages, newsletters, public-account layouts, long-form reading pages. |
|
||||
| `info-interactive.md` | `responsive-html` | Interactive reports, data stories, explorable explainers, filterable information. |
|
||||
| `web-tool.md` | `responsive-html` / `mobile-html` | Single-task web tools: calculators, generators, converters, checkers, pickers, quizzes/tests, decision tools, H5/mobile tools. |
|
||||
| `social-card.md` | `fixed-image` | Social cards, Xiaohongshu/RED cards, WeChat/Douyin covers, screenshot cards, KPI cards, quote cards, image-led covers, long-image card sequences. |
|
||||
| `deck.md` | `html-slide-deck` | Slide decks, pitch decks, report decks, strategy decks, teaching decks, presentation-style HTML. |
|
||||
|
||||
Legacy note: old references to `xiaohongshu-card.md` should route to `social-card.md`; old `tool-h5.md` references should route to `web-tool.md`.
|
||||
|
||||
## System / Reference Skills
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `design-system-reference.md` | Select and apply existing style skills, brand inspiration, open-source design systems, uploaded DESIGN.md, screenshots, or user-owned references. |
|
||||
| `design-system-generation.md` | Import, extract, normalize, or persist reusable `DESIGN.md`. |
|
||||
| `canvas-and-device.md` | Viewport presets, device frames, deck canvases, social-card sizes, long-image/export surfaces, fixed aspect ratios. |
|
||||
| `quality-gate.md` | Final blocking delivery gate for generated or major-edited design artifacts. |
|
||||
| `export.md` | Package, convert, or hand off the current artifact. |
|
||||
| `skills/version-management/SKILL.md` | Parallel version-management skill for editable web entry files (`.html`, `.jsx`, `.tsx`, `.vue`); not a design reference. |
|
||||
|
||||
## Template Library
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `design-templates/INDEX.md` | Human/model-readable template directory: what exists and where. |
|
||||
| `design-templates/TEMPLATE-ROUTER.md` | Template selection rules: carrier gate, conflict handling, and when to select templates. |
|
||||
| `design-templates/TEMPLATE-INDEX.json` | Machine-readable template metadata for tooling or automated retrieval. |
|
||||
|
||||
Template rule: choose the artifact skill first, then use `TEMPLATE-ROUTER.md` to decide whether a template applies.
|
||||
|
||||
## Design Systems
|
||||
|
||||
### `design-systems/`
|
||||
|
||||
Style skills and brand inspiration references. Use `design-systems/index.json` for candidate selection, then read only the matched `DESIGN.md` or style skill.
|
||||
|
||||
The index may contain:
|
||||
|
||||
- `style_skills`
|
||||
- `brand_inspiration`
|
||||
- selection tags such as `industry`, `product_type`, `mood`, `formality`, `density`, `scheme`, `era`, `best_for`, and `avoid_for`
|
||||
|
||||
Keep detailed vocabulary and selection logic inside the design-system reference files, not here.
|
||||
|
||||
## Horizontal Craft
|
||||
|
||||
Read only when the task trigger is present. `SKILL.md` contains the runtime trigger matrix.
|
||||
|
||||
| File | Use when |
|
||||
|---|---|
|
||||
| `horizontal-craft/chinese-typography.md` | Substantial Chinese text or Chinese reading comfort matters. |
|
||||
| `horizontal-craft/reference/title-and-breaking.md` | Titles, covers, slides, social-card headings, or line-break-sensitive Chinese. |
|
||||
| `horizontal-craft/reference/punctuation.md` | Punctuation-heavy Chinese long text. |
|
||||
| `horizontal-craft/reference/text-detail.md` | Font size, line height, paragraph rhythm, dense reading surfaces. |
|
||||
| `horizontal-craft/reference/fonts.md` | Font selection materially affects visual direction. |
|
||||
| `horizontal-craft/reference/grid-system.md` | Reports, long images, publication-like or dense grid layouts. |
|
||||
| `horizontal-craft/reference/directions.md` | Vertical text, classical Chinese, regional typography differences, or traditional layouts. |
|
||||
| `horizontal-craft/color.md` | No design system or color direction is underspecified. |
|
||||
| `horizontal-craft/icon-system.md` | UI/feature/nav/status icons or icon-like marks appear. |
|
||||
| `horizontal-craft/accessibility.md` | Interactive or publish-ready artifacts; also part of final checks. |
|
||||
| `horizontal-craft/animation-discipline.md` | Motion, transitions, scroll effects, animated storytelling. |
|
||||
| `horizontal-craft/form-validation.md` | Forms, inputs, validation, signup, checkout, submission, settings. |
|
||||
| `horizontal-craft/laws-of-ux.md` | Pricing, onboarding, dashboards, H5 tools, conversion or interaction-heavy flows. |
|
||||
| `horizontal-craft/state-coverage.md` | Product, dashboard, form, tool, H5, data, and prototype states. |
|
||||
| `horizontal-craft/data-integrity.md` | Metrics, charts, tables, rankings, percentages, or claims. |
|
||||
| `horizontal-craft/link-and-proof.md` | Links, CTAs, citations, client logos, testimonials, proof claims. |
|
||||
| `horizontal-craft/technique-library.md` | Advanced motion, 3D, shaders, data-viz, or effects beyond plain CSS. |
|
||||
| `horizontal-craft/anti-ai-slop.md` | Final anti-default/anti-template-quality check; light generation reminder, heavier delivery check. |
|
||||
|
||||
## Deprecated / Removed
|
||||
|
||||
- `chinese-copy.md` — removed from core craft. Lightweight AI-flavored copy checks belong in `anti-ai-slop.md` when they affect design quality.
|
||||
- `execution-checklists.md` — removed. Canonical checks live in `SKILL.md` and `quality-gate.md`.
|
||||
- `xiaohongshu-card.md` — replaced by `social-card.md`; keep only as a temporary alias if needed.
|
||||
- `tool-h5.md` — replaced by `web-tool.md`; keep only as a temporary alias if needed.
|
||||
- `METADATA.md` — not required unless a separate tool explicitly consumes it. Prefer `SKILL.md` frontmatter plus this index.
|
||||
Executable
+477
@@ -0,0 +1,477 @@
|
||||
---
|
||||
name: Design Skill
|
||||
description: Route design-related HTML artifact tasks (landing pages, portfolios, prototypes, decks, content pages, web tools, social cards, info-interactive) to the right artifact skill, and decide whether to use design-system-reference, design-system-generation, or export. Use for any UI/visual/HTML design work.
|
||||
---
|
||||
|
||||
# Design Skill(设计技能)
|
||||
|
||||
设计类 HTML 产物的**路由器**:决定任务是否与设计相关、选哪个 artifact skill 作主技能、是否需要 `design-system-reference.md` / `design-system-generation.md` / `export.md`、动手前澄清多少。(产物结构、场景判断、跨场景工艺、参考/导出分别属于各 artifact skill、horizontal-craft/、及对应文件。)
|
||||
|
||||
---
|
||||
|
||||
## 整体流程
|
||||
|
||||
下面是一条流程,大部分步骤是内部的、瞬时完成的——只有当需求**确实含糊**时才停下来询问。**不要**把任何一步变成一张要填的表,或一个必须先读的文件。
|
||||
|
||||
```text
|
||||
用户请求
|
||||
│
|
||||
▼
|
||||
1. 理解三要素(内部进行,瞬时;对明显的需求保持静默)
|
||||
• content(内容) — 主体、信息、目标
|
||||
• scenario(场景) — 形式 + 意图 → 选哪个 artifact skill(见 Routing)
|
||||
• style(风格) — 用户有没有给风格线索(见 Style Routing)
|
||||
内容缺 → 问;场景缺 → 问。(这是唯一"因信息缺失而提问"的地方)
|
||||
同时判定:场景属于「界面类(求稳、求一致)」还是「创意类(求独特、求冲击)」
|
||||
—— 这条决定第 2 步风格怎么呈现、以及后面整体走 design system 还是原创。
|
||||
创意类需求:顺带完成创意解读(Design Read,见 §Creative Context)。
|
||||
│
|
||||
▼
|
||||
2. 确认内容框架 + 风格方向(默认都要确认;逐维度跳过已明确的)
|
||||
⚠️ 这是构建前**必须**的一次暂停——**给出要确认的东西后停下、等用户回复,绝不能跳过它
|
||||
直接去构建完整网站**。检索/研读完资料后直接开建、不先确认,是错误行为。
|
||||
在「同一轮」里把要确认的东西一起给:
|
||||
• 内容框架(大纲):
|
||||
- 需研读(给了材料)/ 需检索(真实主体)/ 写得含糊 → 把大纲列出来确认
|
||||
(研读类要真正读透材料再提架构,不要提取到「勉强够用」就停)
|
||||
- 用户已把内容结构写全(如明确列了要哪几个模块)→ 不再确认,直接用
|
||||
• 风格方向:
|
||||
- 用户已明确风格(说了某风格 / 给了图 / 指定某网站/品牌参考 / 给了 DESIGN.md)
|
||||
→ 不给风格卡,按指定的来
|
||||
- 未明确 + 界面类 → 给风格卡,展示几个不同的成熟 design system 供选(求稳)
|
||||
- 未明确 + 创意类 → 给风格卡,展示几个不同的创意方向供选(求独特)
|
||||
风格卡形态见 §Style Sampling(简单并排卡、A/B/C、绝不 tab)。
|
||||
例:「做一个原研哉的个人网站」→ 创意类、需检索、风格未明确 → 检索其真实作品后,
|
||||
列出内容大纲(哪几块/各放什么作品)+ 给三张创意方向风格卡 → **停下等用户确认并选方向**
|
||||
→ 然后才进入 3 构建。绝不是检索完就直接把整站做出来。
|
||||
两个维度独立判断:都明确→直接进 3;只要有任一维度需要确认,就必须停下等用户。
|
||||
用户回复后进入 3。若用户只调整内容(没换方向),沿用已选方向直接做,不重新出卡。
|
||||
│
|
||||
▼
|
||||
3. 检查模板(仅当场景命中模板库时)
|
||||
在 `design-templates/INDEX.md` 按载体 + 场景检索:明确匹配 → 采用为起点、读其 SKILL.md
|
||||
(通常比从零更好);没有合适的 → 直接用 artifact skill,绝不强行套。
|
||||
模板是起始骨架不是用来克隆——保留用户真实内容,套 artifact skill + horizontal craft。
|
||||
│
|
||||
▼
|
||||
4. 补全风格的剩余维度(在第 2 步已定的大方向内)
|
||||
大方向已在第 2 步定了(指定 / design system / 创意方向之一)。这里只把没被覆盖的
|
||||
具体维度按来源优先级填齐:① 用户即时描述(最高,锁定)② DESIGN.md ③ 模板/种子
|
||||
④ 平行约束兜底(只加载需要的:字体/图标/配色…)。各来源可叠加。
|
||||
冲突:用户描述与设计系统在某维度矛盾时,以用户为准,并用一句话说明差异。
|
||||
│
|
||||
▼
|
||||
5. 开工预告(进入较长构建之前)—— 见 §Build Preview
|
||||
不许只说「稍等」。先用人话给「有内容的施工预告」:做哪几块、怎么呈现、配色/字体、
|
||||
预计耗时。构建中无法实时播报(写文件是一次性工具调用、产品不流式渲染),信息须在
|
||||
开工前一次给足,让等待期有内容可读、能判断方向。
|
||||
│
|
||||
▼
|
||||
6. 构建:用 artifact skill 的判断 + 补全后的风格 + 第 2 步所选方向。
|
||||
│
|
||||
▼
|
||||
7. 引导下一步 —— 每轮都做,靠判断(见 §Guiding the Next Step)
|
||||
看当前情形给「最多一个」最相关的下一步:补全缺口、接到用途(offer 配套)、点出新岔口、
|
||||
诊断当下最弱的维度、指向分享或编辑入口。没合适项就不提;只指路不代劳。
|
||||
│
|
||||
▼
|
||||
8. 质量门禁 —— 交付前把 quality-gate.md 当作精简的最终检查清单跑一遍。
|
||||
(适用于「每一个」产出产物的轮次,不只第一轮——见 §Every turn。)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 界面类 vs 创意类:风格的两条路(核心原则)
|
||||
|
||||
第 1 步判定的「界面类 / 创意类」决定风格怎么定——**这是贯穿全流程的一条主原则**:
|
||||
|
||||
- **界面类**(产品原型、仪表盘、后台、Web 工具、表单/引导/结账、数据密集界面、通用企业/通知页):求成熟与一致,**靠 design system,而非模型自由发挥的品味**。把场景(及任何情绪词)通过 `mood` 标签映射到 `design-systems/style-skills` 条目作 UI 基底——真实系统提供结构、组件纪律、间距节奏、状态与交互惯例。无明确风格时,第 2 步的风格卡展示几个不同 design system 供选。
|
||||
- **创意类**(品牌站、campaign、作品集、落地页 hero、编辑/文化专题、社交卡片、内容页):求独特与冲击,**用 artifact skill 的视觉判断 + horizontal craft + Creative Context 原创**。可*参考*某系统的气质(具名品牌会加载 `brand-inspiration`),但不被塞进通用 UI 套件。无明确风格时,第 2 步的风格卡展示几个不同创意方向供选。
|
||||
|
||||
判断标准一句话:**这个产物要的是"可靠一致"还是"独特出彩"——可靠→design system;出彩→原创(可参考)。** 用户已明确风格(说了风格/给图/给参考/给 DESIGN.md)时,两类都不给卡、直接按指定来。
|
||||
|
||||
---
|
||||
|
||||
可能为第 4 步供料的上下文来源:用户内容、截图、所选的模板/种子、上传的 `DESIGN.md`、当前产物、品牌素材、已有页面、设计系统 / UI 套件、或 Figma/代码库参考。有什么用什么;只有当某个维度没有别的来源覆盖时,才去读对应的平行约束文件。
|
||||
|
||||
---
|
||||
|
||||
## 输出纪律:藏名字,不藏产出(贯穿全流程,最高优先)
|
||||
|
||||
对用户**只藏工序的「名字」,不藏它的「产出」**。
|
||||
|
||||
- **别说出口**:内部工序名 / 机制名(Content Read、Style Sampling、Design Read、Creative Context、Variation Policy 等),以及 skill 的运作逻辑、判断理由。
|
||||
- **照常给**:这些工序的产出——设计分析(「我把它理解为…」)、内容大纲、风格方向(样片)、施工预告、下一步建议、对用户的提问。用人话给,别加「这是我的 Content Read」这种标签。
|
||||
|
||||
拿不准时默认**倾向于给**:只对那几个工序名保持沉默,凡是对用户有用的分析或产出都照常讲清楚——绝不能因为「怕暴露」而把大纲、设计定位、风格方向这些藏掉或一笔带过。
|
||||
|
||||
- ✅「我把你的资料理了个大纲:分 X / Y / Z 三块(完整列出),另外给你三个风格方向看看,你看分得对不对、喜欢哪个?」
|
||||
- ❌「我正在做 Content Read 和 Style Sampling。」(念了名字)
|
||||
- ❌(确认了要研读内容,却不把大纲列出来就闷头做)(藏了产出)
|
||||
|
||||
**特别强调:内容大纲、风格方向这类合并确认轮的产出(见 §Content Read / §Style Sampling),是必须呈现给用户并请其确认的——把「不暴露内部逻辑」理解成「不给大纲」是错误的。** 该暂停确认的就要暂停确认,只是话术里不出现工序名。
|
||||
|
||||
一句话:**对外不报「工序名」,但照常交付「工序的产出」。**
|
||||
|
||||
# 1. 先理解
|
||||
|
||||
这一块决定"做什么之前先想清楚什么"。多数情况内化、即时完成;只有真含糊才问一句。
|
||||
|
||||
## Role(角色)
|
||||
|
||||
这个 agent 是「设计导向」的,不是通用 UI 生成器——它为每个产物带来设计判断:理解产物用途、决定用户最先看什么、删掉配不上位置的元素、依赖通用品味前先用参考、为后续编辑保留结构、产出可用可编辑有意图的东西。
|
||||
|
||||
目标不是生成更多界面,而是创造一个帮用户沟通/测试/发布/演示/交付某件有用东西的设计产物。
|
||||
|
||||
---
|
||||
|
||||
## Content Language(内容语言)
|
||||
|
||||
生成的 HTML 内容语言必须与用户对话语言一致,除非用户明确要求其他语言。
|
||||
|
||||
规则:用户用中文写→产物所有可见文字(标题/正文/标签/CTA/微文案/导航/占位/alt)都用中文;用英文写→英文;混用→跟随主导语言、自然处保留混用(如产品名、术语)。适用所有场景。`<html lang>` 必须与内容语言一致。改用模板时,把占位文字换成正确语言,不保留模板原本的英文占位。
|
||||
|
||||
检测到中文/CJK 内容时(无论由此规则还是用户要求),加载 `horizontal-craft/chinese-typography.md`。**字体铁律(读那个文件之前就生效):`font-family` 里任何非系统字体都必须真正被加载(`<link>` 或 `@font-face`),且含中文的页面 `font-family` 里必须有一个 CJK 字体名——绝不能让拉丁字体直接穿透到 `system-ui`,否则中文会以不受控的兜底字体渲染。** 字体细节见 `horizontal-craft/fonts.md`。
|
||||
|
||||
不要让用户确认内容语言,除非确实含糊(如双语 brief 无明确主导语言)。
|
||||
|
||||
---
|
||||
|
||||
## Core Principle(核心原则)
|
||||
|
||||
默认动手,但不要盲目设计。
|
||||
|
||||
有选择地提问:默认不问一长串问卷,但简短澄清能实质改善结果时就问。否则从以下推断合理假设:用户请求、工作区上下文、提供的内容/参考、所选模板、当前产物状态、上传的 `DESIGN.md`、场景常见模式。
|
||||
|
||||
只在缺失信息会导致以下情况时才问:不可用、无法生成、有法律/品牌风险、或明显与用户意图不符。否则生成一个合理初版,让用户通过选区批注、局部编辑、追问或导出来打磨。
|
||||
|
||||
**例外——有源材料的任务(见 Content Read):** 用户附带源材料、或要呈现真实主体时,不要直接跳去做初版,先读内容、提架构、再确认。"默认动手"管的是抽象/创意请求;有源材料的任务深度来自先读透。
|
||||
|
||||
---
|
||||
## Content Read(内容研读)
|
||||
|
||||
这是第 1 步的「内容轴」——与 Creative Context(风格轴)和 Routing(场景轴)平级。流程里点名了三条轴(content / scenario / style);这里就是内容轴真正落实的地方。它**只适用于「有源材料」的任务**,定义为:
|
||||
|
||||
- 用户附带了要呈现或据以构建的材料:PDF、文档、演示稿、数据集、表格、图片集、已有的网站/文本;或
|
||||
- 任务是要呈现一个带真实信息的**真实主体**:真实作品的作品集,个人 / 公司 / 产品 / 活动介绍,报告或数据故事,"介绍 XX / 做一个关于 XX 的网站"。
|
||||
|
||||
它**不适用于**没有真实来源的抽象生成("做个读书会活动页""一个 AI 工具的落地页""做张海报")——那些走 Default Assumption Policy 的 scaffold、留在快速路径上。分界线是:**到底有没有真实内容需要理解,还是我们在凭空造一个合理的起点?**
|
||||
|
||||
对有源材料的任务,在做结构 / 风格 / 模板 / 构建之前:
|
||||
|
||||
```text
|
||||
Content Read(仅限有源材料的任务)
|
||||
1. read_through(读透): 真正把整份源材料读完——不要提取到「勉强够用」就停。
|
||||
「略读就开做」正是让产出变薄的失败模式。
|
||||
2. extract(提取): 抽出真实的信息块。作品集:每个项目的
|
||||
挑战 / 你做了什么 / 结果 + 哪些图属于它。
|
||||
介绍/报告:关键事实、章节、图表、引述。
|
||||
理解每个素材的含义和它该放哪——绝不只按文件大小或
|
||||
出现顺序挑图。
|
||||
3. architect(搭架): 从内容中推导信息架构——有哪几个区块、每块讲什么、
|
||||
用户最该先看到什么、素材→区块对应。
|
||||
结构从材料里长出来,不是来自某个场景的默认坑位,
|
||||
也不是来自模板的区块清单。
|
||||
4. confirm(确认): 在「一轮」里,把内容框架(一份简短大纲:有哪些区块、
|
||||
每块讲什么、首位/最强项是哪个、素材对应)连同三个可切换的
|
||||
风格样片(见 §Style Sampling)一起呈现。请用户在一条回复里
|
||||
确认框架并选一个方向。一轮搞定——是「大纲 + 三个首屏样片」,
|
||||
不是审问。然后进入第 3 步往后。
|
||||
```
|
||||
|
||||
深度,而非堆量:目标是真正读懂并组织好内容,不是更多区块或更长文案。这服务于、绝不凌驾于「禁止造假/反 AI 套路」——读透不等于编造或注水;源材料确实缺的(如某项目没结果)就说明或用标注清楚的占位,不捏造。
|
||||
|
||||
**呈现要求(务必遵守):** confirm 这步必须把内容大纲**真实、完整地列在回复里**(哪几块、每块讲什么、首位、配图归属),连同三个风格方向一起请用户确认,然后停下等回复,不要自行往下做完整页。大纲是要交付给用户的产出,**不受输出纪律隐藏**(只是别念「Content Read」这词,不是省略大纲)。藏大纲直接开做是错误行为。这个合并轮把「讲什么+长什么样」一次对齐,避免「做错方向白做 5 分钟」;抽象任务无此暂停。
|
||||
|
||||
---
|
||||
|
||||
## Style Sampling(风格样片)
|
||||
|
||||
风格样片与内容框架在「同一个确认轮」里产出(流程第 2 步),不是后面单独的步骤。这是有意为之、并不浪费:它让视觉方向「靠看来选」(用户很难从文字描述里可靠地选出风格,尤其在没有具名设计系统可截图时),它消除了整页返工的最大来源("不是这个感觉 → 全部重做"),并在一轮里制造一个自然的、高质量的决策点。**给出样片后必须停下、等用户选 A/B/C,绝不能自己替用户选一个方向就直接去做完整网站。**
|
||||
|
||||
```text
|
||||
Style Sampling(与内容框架一起产出,第 2 步)
|
||||
0. 先简述方向: 先用一两句话点出三个方向的意图/调性(只说气质、不锁死配色字体),
|
||||
再在同一条回复里紧接着生成样片 HTML。
|
||||
1. one file(一个文件): 三个方向放进「一个 HTML 文件」,在「同一页里并排」展示。
|
||||
⚠️ 必须并排,绝不能用 tab / 切换 / 折叠——切换要来回点、看不到对比,
|
||||
失去了样片的全部意义。桌面三栏并排;移动端窄屏纵向堆叠。
|
||||
绝不输出三个独立文件/预览——产品一次只显示一个产物。
|
||||
2. sample 形态: 每个方向做一张「简单的风格卡」——不是迷你网页、不是高保真缩略页。
|
||||
目的只是让用户一眼感受风格方向,所以保持轻:一个标题样例(体现字体
|
||||
气质)、一两行示意文字 + 一个示意按钮/标签(体现基础排版与留白)、
|
||||
该方向的配色(背景+文字+强调色)。⚠️ 不要做完整 hero、不要铺真实内容
|
||||
区块、不要精修到接近成品——那样既慢又重,也不是这步要的。三张卡加起来
|
||||
应很快很轻,远小于一个完整页。"做得精致"留到选定方向后的完整构建。
|
||||
⚠️ 小样阶段不收集、不嵌入真实图片——配图是「完整构建」才做的事;这步若要
|
||||
示意图位,只用最简单的占位色块即可,绝不为小样去找图/生成图(那会拖慢小样)。
|
||||
3. label(标识): 每张样片左上角标一个醒目的 A / B / C 角标,底部标题也带编号
|
||||
("A · 墨色展厅" / "B · 宣纸暖白" / "C · 现代无衬线"),并各配一行
|
||||
一句话方向描述 + 配色圆点。让用户能一句话指认("我要 A")。
|
||||
4. content(内容): 三个用同一份已确认内容。
|
||||
5. distinct(差异化): 三者要在真实维度上不同——配色 / 字体气质 / 整体调性,不是表面装饰。
|
||||
6. pick(选择): 请用户在一条回复里点一个("A/B/C 哪个"),也可同时调整框架;
|
||||
然后按所选方向把完整页做出来,覆盖所有已确认区块。
|
||||
```
|
||||
|
||||
关键拿捏:**样片要「轻」。** 最常见的跑偏是做成三个高保真缩略页(慢、重,还容易退化成 tab)——要避免;一眼能看出配色/字体气质/调性的差别就够,精致度留到选定后。它与 §Content Read 合成有源材料任务唯一的前置:一轮里同时给「讲什么(内容框架)+ 长什么样(三张样片)」→ 用户确认并选方向 → 一次有把握的完整构建。样片只在初次构建做一次,之后是普通局部/迭代编辑,不重复出样片。
|
||||
|
||||
---
|
||||
|
||||
## Guiding the Next Step(引导下一步)
|
||||
|
||||
design 任务的下一步抓手,按情形选:
|
||||
|
||||
- **有缺口** → 补全(最典型:占位图 → 请用户发真实图替换)。
|
||||
- **已成型** → 接到用途上:用途未知就问一句并 offer 配套。天然配套:落地页→社交卡片/邮件版;deck→演讲备注;作品集→单项目详情页。
|
||||
- **暴露出新的方向抉择** → 点出(如「这版偏营销感,求职用应更收敛——哪种?」)。样片轮已选定的方向不重开。
|
||||
- **可再提升** → 挑**当下最弱**的维度诊断并 offer 修复:信息层次 / 转化动线 / 密度留白 / 一致性 / 图文配比 / 动效入场 / 氛围音乐 / 说服力区块。粗(结构/动线)先于细(间距/微调);已解决不重开,用户锁定的永久关闭;已够完整或风格不宜则不提。
|
||||
- **口述小改** → 提示预览的编辑入口直接做更快。
|
||||
- **想发布** → 指向预览上方的分享按钮。
|
||||
|
||||
分享/编辑只指路不代劳;提议的优化经同意后由 agent 做。
|
||||
|
||||
---
|
||||
|
||||
## Build Preview(开工预告:进入较长构建之前)
|
||||
|
||||
完整页构建要花一两分钟,用户最容易在一句「稍等」加黑屏前流失。**构建中无法实时播报**(写文件是一次性工具调用、产品不流式渲染,「边写边报」做不到,别承诺)。所以信息要在**开工前一次给足**:进入构建前先用人话给一段**有内容的施工预告**——做哪几块、每块大致怎么呈现、配色/字体方向、诚实的耗时预期,然后才开工。
|
||||
|
||||
示例:「这版:hero 用他的设计理念做大字、墨色留白;作品区分无印良品/字体/HOUSE VISION 三段各配代表作;配色墨黑+宣纸米白,标题思源宋体。约 1–2 分钟。」
|
||||
|
||||
它把「失联空等」变成「知情等待」,还顺带再对齐一次方向(看出不对可当场喊停)。**「开始做了,稍等片刻」不满足要求**——预告必须有具体内容(区块+呈现+配色字体+耗时)。几秒就完成的小改动不必预告。
|
||||
|
||||
---
|
||||
## Creative Context Completion(创意语境补全)
|
||||
|
||||
**定方向的方法:用一个具体参照物,而不是一串形容词。** "现代/干净/可信/高级"什么都没指定——模型只会做出落在这些词中心的东西,通常平庸;形容词描述的是一个「区域」,一个具体参照描述的是一个「点」。换成一个具体的世界(如"1970 年代某老牌大学的研究生讲义"、"东京某独立香水店的极简包装"、"90 年代日本杂志的版式"),它一句话就带出配色、字体、留白、有无装饰——而且自带「它不是什么」(讲义不会发光、不用渐变,不必另行声明)。所以创意定位优先落到一个具体参照,再由它推导细节。
|
||||
|
||||
对开放式生成,在构建前,静默地补全一份简短的创意语境(每项一句话;用户给了就用,否则从场景/受众/内容/参考推断;除非有用否则不展示给用户):
|
||||
|
||||
```text
|
||||
Creative Context
|
||||
1. creative_positioning(创意定位): 这件事真正关乎什么,超越字面请求(尽量落到一个具体参照物)
|
||||
2. first_impression(第一印象): 观众在头 3 秒应该感受到什么
|
||||
3. anti_default(反默认): 要避开哪个通用默认
|
||||
```
|
||||
|
||||
产物必须在视觉上体现这一点。
|
||||
|
||||
### 生成前先说一句 Design Read(设计解读)
|
||||
|
||||
然后用一句明确的话点出你是怎么解读这个请求的:
|
||||
|
||||
格式:**"我把它理解为:面向<受众>的<页面类型>,<气质/调性>,倾向<设计方向 / 系统 / 参考>。"**(如:"面向技术买家的 SaaS 落地页,冷静精确,倾向类 Linear 的克制系统。")
|
||||
|
||||
**这句设计分析要实际说给用户**(它是 B 类产出,不受输出纪律的隐藏约束——隐藏的只是「Design Read」这个词,不是这句分析本身;别加「这是我的 Design Read」这种标签,但分析照常给)。它让用户一眼确认你方向对不对。若某条轴确实含糊,就在这里问那一个聚焦的问题,而不是猜。
|
||||
|
||||
**Design Read 必须以一个「风格来源」子句结尾——必填,三选一,绝不留空:**
|
||||
|
||||
- **界面场景**(原型、信息交互、Web 工具、仪表盘/后台):点名一个系统——`"倾向 <style-skill>(由情绪标签匹配)"`,或在指定了品牌时 `"倾向来自 brand-inspiration 的 <brand>"`。默认用成熟系统;不要从零造 UI。
|
||||
- **创意场景**(落地页、作品集、campaign、卡片、内容):`"参考 <system> 的 <气质>,原创布局"` 或 `"原创方向:<一句话>"`。两者都成立——"原创"是合法来源;要点是你考虑过库。
|
||||
- **任意场景 + 指定品牌** → `"倾向来自 brand-inspiration 的 <brand>"`。
|
||||
|
||||
### 然后按界面/创意分类;创意(表达型)场景要承诺一个具体的视觉方案
|
||||
|
||||
沿用第 1 步的判定(界面类 vs 创意类,详见前面「风格的两条路」核心原则)。**创意/表达型**(品牌站、campaign、作品集、落地页 hero、编辑/文化专题、社交封面):视觉冲击是任务的一部分,一个正确但保守的居中页面就是失败,要做下面的视觉承诺。**界面/克制型**(仪表盘、后台、工具、表单、通知、报告):清晰冷静才是任务,动效最少、布局规整,跳过视觉承诺。
|
||||
|
||||
对**创意/表达型**,在写代码前静默承诺——每项都是一个具体元素 + 动作,绝不是形容词("大胆/高级/惊艳"被禁用,它们什么都没约束):
|
||||
|
||||
```text
|
||||
Visual commitment(仅表达型场景)
|
||||
1. hero_subject: 被做大/做主的那「一个」元素(如产品名占 1/3 视口、特定配色)
|
||||
2. entrance: 哪些元素入场、怎么入场
|
||||
3. break_symmetry: 布局在哪里打破居中/网格
|
||||
4. focal_contrast: 什么让视线最先落定(尺寸跳变 / 色块 / 留白)
|
||||
```
|
||||
|
||||
一个完全居中、对称、静态、没有主元素的页面,就是默认的「平庸」结果——对表达型场景,这意味着承诺没被兑现。逐项执行。动效细节 → `horizontal-craft/animation-discipline.md`。
|
||||
|
||||
|
||||
## 为抽象请求补全内容骨架(不捏造事实)
|
||||
|
||||
抽象请求("做个读书会活动页")要的是可编辑的**起点**而非空壳:用具体可信的示例内容把场景所需框架搭全(一个几乎空白的页面即便技术正确也是失败),写具体示范文案(像真的活动名、三个具体议程项)而非 `Lorem ipsum` 或空占位。**骨架只用于无源材料的抽象请求**——有源材料/真实主体的,区块和内容来自 Content Read 并经确认。
|
||||
|
||||
**这不是捏造,分界:** 可自由填(结构性/示范性:区块结构、卖点文案、议程、FAQ、示例产品名、标注清楚的占位图坑位);**绝不捏造**(事实性/凭证性:具体指标、具名客户 logo、用户评价、奖项、媒体报道、评分——任何断言现实事实的东西,没真实凭证就用标注清楚的占位,绝不编造)。
|
||||
|
||||
---
|
||||
# 2. 路由 —— 决定做什么
|
||||
|
||||
先定形态与意图,再定产出格式与风格来源。
|
||||
|
||||
## Routing(路由)
|
||||
|
||||
按**主 artifact skill** 路由(不要用独立的「形态分桶」路由层)。输出形态(`output_target`)由所选 artifact skill + 用户请求隐含决定,用来定技术形态/尺寸/导出/支撑参考,不必单独记录、也不是一个路由层。
|
||||
|
||||
## Primary Routing — Artifact Skills(主路由 —— 产物技能)
|
||||
|
||||
为一个具体设计产物,精确挑选一个主 artifact skill。该 artifact skill 定义形态、结构、输出机制和编辑预期。
|
||||
|
||||
| Artifact skill | 当用户想要…… | 默认 output target |
|
||||
|---|---|---|
|
||||
| `landing-page.md` | 产品落地页、首页、SaaS 站、营销页、waitlist、服务页 | `responsive-html` |
|
||||
| `portfolio.md` | 个人站、作品集、创作者主页、简历式页面、项目展示 | `responsive-html` |
|
||||
| `prototype.md` | app 原型、Web 产品原型、产品/功能 demo、仪表盘、后台面板、产品流程、线框、UI mockup | `responsive-html` 或 `mobile-html` |
|
||||
| `content-page.md` | 文章、编辑型页面、newsletter、微信/公众号排版、长文阅读页 | `responsive-html` |
|
||||
| `info-interactive.md` | 信息讲解页、可视化讲解、流程图、架构图、系统图、流程图谱、关系图、时间线、对比浏览、可筛选的知识/资源页、交互式报告、数据故事、可探索的讲解;日常语言触发词:"讲清楚 / 梳理 / 图解 / 可视化说明 / 流程图 / 架构图 / 关系图 / 时间线 / 对比页" | `responsive-html` |
|
||||
| `web-tool.md` | 单任务 Web 工具、计算器、生成器、检查器、选择器、quiz、测试、倒计时、决策工具;桌面或移动/H5 | `responsive-html` 或 `mobile-html` |
|
||||
| `social-card.md` | 社交媒体封面/卡片、小红书、微信/抖音封面、长图卡片序列、固定尺寸社交图 | `fixed-image` |
|
||||
| `deck.md` | 幻灯片、演示、pitch deck、报告 deck、策略 deck、工作坊 deck、教学 deck | `html-slide-deck` |
|
||||
|
||||
### 歧义词:demo
|
||||
|
||||
"demo / 做个 XX 的 demo / 演示" 默认 = **可交互、可点击的产品原型**(`prototype.md`,双文件交付 prototype.html + flow.html),**绝不**默认成营销/活动页。仅当请求明确带营销意图(落地页/官网/推广/waitlist/活动页)→ `landing-page.md`;是单任务小工具 → `web-tool.md`;是路演 slides → `deck.md`。若主体无法被交互演示,问一句,别默默降级成展示页。
|
||||
|
||||
## Output Target(输出目标)
|
||||
|
||||
用它定技术形态、尺寸、导出行为和支撑参考(由 artifact skill + 用户请求推导,不是路由层):
|
||||
|
||||
| Output target | 含义 | 常见支撑参考 |
|
||||
|---|---|---|
|
||||
| `responsive-html` | 可滚动或可交互的浏览器页面 | `canvas-and-device.md`, `horizontal-craft/accessibility.md` |
|
||||
| `mobile-html` | 移动优先的交互页、H5 或移动 Web 工具 | `canvas-and-device.md`, `horizontal-craft/state-coverage.md`, `horizontal-craft/form-validation.md` |
|
||||
| `html-slide-deck` | 一屏一页的 HTML 演示 | `deck.md`, `design-templates/`, `canvas-and-device.md` |
|
||||
| `fixed-image` | 社交卡片、封面、海报、长图或图片导出的固定尺寸导出面 | `social-card.md`, `canvas-and-device.md` |
|
||||
| `design-system-spec` | 可复用的 `DESIGN.md` / token / 组件规则 | `design-system-generation.md` |
|
||||
| `package` | ZIP/PDF/PPTX/图片/导出交付 | `export.md` |
|
||||
|
||||
## Style Routing(风格路由;决定一个风格线索怎么处理)
|
||||
|
||||
请求常带一个风格线索。在动用参考库之前,先给线索分类——**大多数线索都不是一次参考查找**。这能防止两种失败:把一个普通形容词当成库查找,以及把"读一份规范"和"写一份规范"搞混。
|
||||
|
||||
| 用户的风格线索是…… | 当作…… | 它去哪里 |
|
||||
|---|---|---|
|
||||
| 一个形容词 / 情绪词("科技感"、"简洁"、"高级感") | 按界面类/创意类分流 | 见「风格的两条路」核心原则(界面类→映射 style-skill;创意类→当创意方向) |
|
||||
| 一个具名品牌("Apple style"、"like Stripe"、"苹果风格")——**任意场景** | 一个**品牌参考** | `skills/design/design-system-reference.md` → `design-systems/brand-inspiration/`(网站/品牌级;读该品牌的 DESIGN.md) |
|
||||
| 一个具名 style skill("用 `minimal` 风格"、"atelier-zero") | 一个**style skill** | `skills/design/design-system-reference.md` → `design-systems/style-skills/` |
|
||||
| 一个要遵循的现有系统(上传的 DESIGN.md、截图、之前的产物、"和我们产品一致") | 一个**提供的参考** | `skills/design/design-system-reference.md`(与给定来源保持一致) |
|
||||
| "提取 / 定义 / 产出一份可复用的 DESIGN.md" | 一个**生成任务** | `design-system-generation.md` |
|
||||
|
||||
关键区分(两点,①已见核心原则不赘述):② **品牌词总是命中 brand-inspiration**(官方外观,最适合营销/创意页)。③ **读 vs 写 DESIGN.md**——"用 Apple 风格"是把品牌当参考读、交付物仍是该页面;只有"产出一份可复用 DESIGN.md"才用 `design-system-generation.md` 写新规范。
|
||||
|
||||
风格线索绝不改变主 artifact skill("Apple 风格的落地页"仍是 `landing-page.md`)。
|
||||
|
||||
---
|
||||
|
||||
## 产物技能消歧(路由表分不清的边界)
|
||||
|
||||
- **social-card vs content-page**:卡片/封面/固定尺寸图/小红书/微信抖音封面/截图卡/KPI卡/引言卡/长图序列 → `social-card.md`;线性阅读页 → `content-page.md`。
|
||||
- **content-page vs info-interactive**:阅读 → `content-page.md`;理解/图解/讲解/对比/筛选/可视化("梳理一下""做成图解""流程图""架构图""时间线""对比页")→ `info-interactive.md`,即便产物大多是静态 HTML/SVG。
|
||||
- **info-interactive 边界**:讲解*信息*而非演示*产品*;固定信息体而非实时数据;若载体明确是幻灯/社交图,用那个载体 skill + `horizontal-craft/visual-explanation.md`。
|
||||
- **尚不存在的 skill 用替代**:dashboard → 以仪表盘为重心的 `prototype.md`;mobile app → `prototype.md` + `platform: mobile`;海报 → `social-card.md`。skill 缺失就用最接近的,只在影响执行时才指出缺口。
|
||||
|
||||
# 3. 做好它 —— 执行、质量、参考
|
||||
|
||||
## Execution essentials(执行要点)
|
||||
|
||||
### 只在触发条件出现时才加载某个参考
|
||||
|
||||
这是「何时读什么」的唯一依据。默认一个都不读;每个任务拉 1–3 个相关文件。
|
||||
|
||||
```text
|
||||
quality-gate.md → 每个生成 / 大改的 HTML 产物(强制)
|
||||
canvas-and-device.md → 固定画布、设备/手机/平板/桌面预览、deck 画布、社交卡片、长图、导出尺寸、固定宽高比
|
||||
design-system-reference.md → 存在品牌 / 风格 / 模板 / DESIGN.md 参考;或产品/界面密集型产物没给风格方向、用成熟基底能提升一致性
|
||||
horizontal-craft/color.md → 没有设计系统、或配色方向定义不足
|
||||
horizontal-craft/accessibility.md → 交互或可发布的产物;质量门禁里也会检查
|
||||
horizontal-craft/animation-discipline.md → 动效、转场、滚动效果或动画叙事
|
||||
horizontal-craft/form-validation.md → 表单、输入、校验、提交、注册、结账或设置界面
|
||||
horizontal-craft/laws-of-ux.md → 定价、引导、仪表盘、H5 工具、转化流程或交互密集型产物
|
||||
horizontal-craft/icon-system.md → 任何 UI / 功能 / 导航 / 状态图标、象形符号、类图标标记
|
||||
horizontal-craft/chinese-typography.md → 大量中文 / 中文阅读舒适度(应用其 Mandatory Runtime Baseline)
|
||||
→ reference/title-and-breaking.md → 标题 / 封面 / PPT / 小红书 标题断行
|
||||
→ reference/punctuation.md → 标点密集的长中文文案
|
||||
→ reference/text-detail.md → 字号 / 行高 / 间距 / 段落节奏
|
||||
→ reference/fonts.md → 字体选择、正式报告排版
|
||||
horizontal-craft/state-coverage.md → 产品 / 仪表盘 / 表单 / 工具 / H5 / 数据 / 原型 界面
|
||||
horizontal-craft/data-integrity.md → 指标、图表、表格、排名、百分比或主张
|
||||
horizontal-craft/link-and-proof.md → 链接、CTA、引用、客户 logo、用户评价、凭证主张
|
||||
horizontal-craft/visual-explanation.md → 流程图、架构/系统图、时间线、关系图、流程图、对比可视化、图表、地图、可探索模型,或任何可视化讲解组件
|
||||
horizontal-craft/technique-library.md → 超出纯 CSS 的效果能服务于目标(动效 / 3D / 数据可视化)
|
||||
```
|
||||
读了参考还不够——规则必须真正改变产物。除非 HTML/CSS/JS 里真的包含实现,否则不要声称某个门禁已应用。
|
||||
|
||||
### 交付前(阻塞)
|
||||
|
||||
```text
|
||||
[ ] quality-gate.md 作为阻塞清单执行(不是被动参考)
|
||||
[ ] 交互产物有 :focus-visible;有动效时有 reduced-motion
|
||||
[ ] 没有假凭证、emoji 当图标、死链、占位逻辑或缺失素材
|
||||
```
|
||||
若某个门禁过不了,修复产物或明确标出未解决的问题——绝不默默忽略。
|
||||
|
||||
### 每一轮,不只是第一轮(多轮规则)
|
||||
|
||||
上面的流程和「交付前」清单适用于**每一个创建或实质性修改产物的轮次**——不只是对话里的第一个任务。后续轮次容易以「前面的轮次已经做过了」为借口默默丢掉步骤(最常见的是质量门禁和版本管理)。它们没有:**每次交付都独立把关。第 8 轮的新任务就是一个新任务,不是延续。**
|
||||
|
||||
任何产物产出轮次(包括"改一下 XX"的追问、以及对话中途切换到不同场景)的最小循环:
|
||||
|
||||
1. 重新确认场景(可能随新需求变了——若是则重新路由)。
|
||||
2. 做改动。
|
||||
3. 在修改后的产物上重跑 `quality-gate.md`。唯一豁免:对从未走过 Design Skill 流程的文件做纯文本/颜色编辑(见 version-management skill §0)。
|
||||
4. 交给版本管理。
|
||||
|
||||
若在后续任何一轮你不确定某条规则的确切内容,重新读相关文件,而不是凭记忆猜。
|
||||
|
||||
## Built-In Quality Defaults(内置质量默认)
|
||||
|
||||
不要把通用渐变/卡片网格/bento/圆角卡片当成自动风格——有意识选择形态(见 `horizontal-craft/anti-ai-slop.md`)。尺寸/视口/导出比例/设备框约束重要时用 `canvas-and-device.md`。
|
||||
|
||||
**图文并茂(适合配图的场景,仅在完整构建阶段):** 做完整页时(不是风格小样阶段——小样只占位、不收图),设计落地页、作品集、内容/编辑页等视觉型产物默认就规划图文结构、为图片留出位置(hero 图、配图、作品图、图标等),而不是堆纯文本——纯文本排列是常见的偷懒结果。没有真实图时用**得体的占位**:合适的比例/尺寸、克制美观的占位样式(柔和底色或细边框,而非刺眼灰块)、并标注该放什么图(如「[产品主图]」),让页面在填图前也完整好看。然后按 §Guiding 引导用户发真实图替换。(纯工具/仪表盘/表单等功能型产物图不是重点,不强求配图。)
|
||||
|
||||
## HTML Artifact Structure(HTML 产物结构)
|
||||
|
||||
把生成的 HTML 组织成便于后续编辑——稳定的 section id / data 属性:
|
||||
|
||||
```html
|
||||
<section id="hero" data-section="hero"> <!-- pages: hero/work/about/contact -->
|
||||
<section class="slide" data-slide="cover"> <!-- decks -->
|
||||
<section data-screen="dashboard"> <!-- prototypes: + data-state, data-component -->
|
||||
```
|
||||
|
||||
- 让 section / slide / screen / component / state 保持可识别,便于选区编辑
|
||||
- 优先可读的语义化 HTML;不要把内容藏在不透明的生成块里
|
||||
- 把占位内容标注为占位;可发布产物里不留死的 `href="#"`,除非已标注
|
||||
|
||||
---
|
||||
|
||||
## 参考与系统:硬约束
|
||||
|
||||
何时加载哪个文件已由上面的触发矩阵决定。这里只记各自「会做错」的硬约束:
|
||||
|
||||
- **中文排版**:除非 CSS 对字体栈/字号/行高/行宽/字间距/段落节奏/标点/中-拉-数字混排做了具体决定,否则不算应用 `chinese-typography.md`。
|
||||
- **模板**:是结构种子不是用来克隆的页面。只读所选模板的 `SKILL.md`,仅当缺结构细节才看 `pattern.html` 且只取布局节奏/区块关系/响应式/交互——**绝不照抄 DOM、类名、token、占位文字、样式**。没匹配就直接用 artifact skill。
|
||||
- **公开品牌**(Apple/Stripe/Linear…):借鉴其美学特质做原创设计;效果优先,用了受版权素材一句话提示风险即可,不为规避而降质,也不主动伪称官方出品。**开源系统**:按公开文档与许可用官方组件,不与该公司私有品牌混为一谈。
|
||||
- **技法库**:`technique-library.md` 是参考不是路由目标。默认简单 CSS,只在效果服务于目标时升级、始终带 reduced-motion,绝不为新奇而用。
|
||||
- **设计系统参考**:`design-system-reference.md` 只加载最相关那一个,绝不全加载。
|
||||
- **参考优先级**:用户给的 > 设计系统 > 模板/场景模式 > 通用知识 > 模型自由发挥。
|
||||
|
||||
# 4. 交付与收尾
|
||||
|
||||
## Web entry / version management(Web 入口 / 版本管理)
|
||||
|
||||
`quality-gate.md` 通过后,**当且仅当交付物含入口前端文件**(`.html`/`.jsx`/`.tsx`/`.vue`)时运行 `skills/version-management/SKILL.md`(按输出类型触发,不是场景)。它管项目路径/素材/git/版本卡片;只贴在对话里或存到工作区根目录**不算**交付。纯图片/文档/PPTX/JSON/DESIGN.md/无新 Web 入口的导出包则跳过。
|
||||
|
||||
---
|
||||
|
||||
## Export Skill(导出技能)
|
||||
|
||||
当用户要求导出、下载、打包、交付或转换产物时用 `export.md`。目标:独立 HTML、ZIP、图片/截图、PDF、PPTX、JSON/元数据、设计规范、DESIGN.md 包。导出必须保留当前产物状态——不要在导出时默默重新设计。
|
||||
|
||||
---
|
||||
|
||||
## IP and Content Boundaries(知识产权与内容边界)
|
||||
|
||||
**效果优先,不为版权把事情做绕或降质。** 尤其**不要**为了规避版权而用劣质替代——例如本该用真实图片/素材的画面,全部改用手绘 SVG 重画一遍,这只会降低质量、违背目标。正确做法:**风格可自由借鉴**(提取某品牌/作品的美学特质——配色、氛围、构图、排版——做原创设计,风格不受版权保护,鼓励这么做);**需要真实受版权素材**(具体图片、logo、作品画面)时,优先用得体的占位 + 引导用户提供有权使用的版本,而不是降质硬凑。
|
||||
|
||||
如果产物里确实用了可能受版权保护的内容(用户提供的、或按要求引入的品牌素材/图片/字体),**用一句话提示用户自行确认使用权/版权风险**即可——这是提示,不是拒绝做事,也不要因此降低成品质量。最终成品效果是目标。
|
||||
|
||||
唯一底线:不主动伪造「这是某公司官方系统/官方出品」的虚假声明(除非用户提供授权)。
|
||||
|
||||
未经允许不扩大范围(加用户没要的区块/页面前先问),但把被要求的页面充实成完整可信的骨架是应该的(充实≠注水)。
|
||||
|
||||
---
|
||||
|
||||
## Edit Handling(编辑处理)
|
||||
|
||||
**选区批注编辑**:以选区为主要目标,优先局部编辑而非重写、保留无关区块;含糊时选最安全的局部编辑或问一句。**局部参数编辑**(颜色/间距/圆角/密度/字号/设备预览):除非影响设计意图/内容结构/视觉方向,否则交确定性局部工具、不调模型 skill。
|
||||
Executable
+327
@@ -0,0 +1,327 @@
|
||||
---
|
||||
name: Canvas and Device Constraints
|
||||
description: Shared sizing, viewport, device-ratio, and export-surface rules for design artifacts.
|
||||
mode: reference
|
||||
platform: any
|
||||
scenario: canvas-device-constraints
|
||||
preview:
|
||||
type: none
|
||||
default_for:
|
||||
- device size
|
||||
- viewport
|
||||
- canvas
|
||||
- resolution
|
||||
- aspect ratio
|
||||
- phone frame
|
||||
- desktop frame
|
||||
- export size
|
||||
- 尺寸
|
||||
- 分辨率
|
||||
- 画布
|
||||
- 手机比例
|
||||
- 导出尺寸
|
||||
fidelity: system
|
||||
---
|
||||
|
||||
# Canvas and Device Constraints
|
||||
|
||||
Use this shared reference whenever a design artifact includes device previews, fixed export sizes, phone frames, dashboard canvases, deck canvases, social cards, long images, or image-export surfaces.
|
||||
|
||||
Core rule:
|
||||
|
||||
```text
|
||||
Artifact canvas ≠ device screen canvas ≠ export surface
|
||||
```
|
||||
|
||||
A flow diagram may use a wide canvas. A phone preview inside it must still preserve a real phone ratio. A social card may use a fixed image export ratio. A dashboard may use a desktop viewport. Do not stretch everything into generic cards.
|
||||
|
||||
---
|
||||
|
||||
## Extensible size registry
|
||||
|
||||
This file is the canonical place for platform size presets. Do not delete it when simplifying routing. Add new platform presets here instead of scattering dimensions across artifact skills.
|
||||
|
||||
Use this registry for:
|
||||
|
||||
- Xiaohongshu / RED covers, notes, long images, and share cards
|
||||
- WeChat article covers, article images, and long images
|
||||
- Posters, flyers, social cards, and story images
|
||||
- Mobile app, mobile web, and H5 viewports
|
||||
- Desktop web, dashboard, and deck canvases
|
||||
|
||||
When a platform has multiple valid sizes, record them as named presets. Artifact skills should reference these presets instead of hard-coding dimensions.
|
||||
|
||||
Recommended preset marker format:
|
||||
|
||||
```html
|
||||
<section data-platform="xhs" data-preset="cover-3x4" data-export-ratio="3:4">
|
||||
<section data-platform="xhs" data-preset="portrait-4x5" data-export-ratio="4:5">
|
||||
<section data-platform="xhs" data-preset="square" data-export-size="1080x1080">
|
||||
<section data-platform="wechat" data-preset="article-cover">
|
||||
<section data-platform="poster" data-preset="a4-portrait">
|
||||
```
|
||||
|
||||
### Current social/export presets
|
||||
|
||||
```text
|
||||
Social square: 1080 × 1080
|
||||
Social portrait: 1080 × 1350
|
||||
Story / vertical: 1080 × 1920
|
||||
Xiaohongshu cover: 3:4 or 4:5
|
||||
Xiaohongshu square: 1:1, usually 1080 × 1080
|
||||
Xiaohongshu long image: width 1080, height content-driven
|
||||
WeChat long image: width 750 or 1080, height content-driven
|
||||
Poster portrait: ratio-driven, preset required
|
||||
```
|
||||
|
||||
If a user provides a platform-specific size, use the user-provided size and add it to this registry if it is likely to be reused.
|
||||
|
||||
### User-provided Chinese social media presets
|
||||
|
||||
These presets are collected from the user's provided social-media size reference image. Treat them as practical production presets, not as immutable official platform specifications. If a platform updates its recommended sizes, update this section.
|
||||
|
||||
#### Xiaohongshu / RED
|
||||
|
||||
| Preset | Size | Ratio | Use |
|
||||
|---|---:|---:|---|
|
||||
| `xhs-avatar` | 400 × 400 | 1:1 | 头像 |
|
||||
| `xhs-profile-background` | 1000 × 800 | 5:4 | 个人背景图 |
|
||||
| `xhs-note-cover-portrait` | 1242 × 1660 | ~3:4 | 图文封面,竖版 |
|
||||
| `xhs-note-cover-square` | 1080 × 1080 | 1:1 | 图文封面,方版 |
|
||||
| `xhs-note-cover-landscape` | 2560 × 1440 | 16:9 | 图文封面,横版 |
|
||||
| `xhs-video-cover-portrait` | 1080 × 1440 | 3:4 | 视频封面,竖版 |
|
||||
| `xhs-video-cover-landscape-4x3` | 1440 × 1080 | 4:3 | 视频封面,横版;source image labels it as 3:4, but the pixel ratio is 4:3 |
|
||||
| `xhs-video-cover-landscape-16x9` | 1920 × 1080 | 16:9 | 视频封面,横版 / widescreen |
|
||||
|
||||
Recommended markers:
|
||||
|
||||
```html
|
||||
<section data-platform="xhs" data-preset="xhs-note-cover-portrait" data-export-size="1242x1660" data-export-ratio="3:4">
|
||||
<section data-platform="xhs" data-preset="xhs-note-cover-square" data-export-size="1080x1080" data-export-ratio="1:1">
|
||||
<section data-platform="xhs" data-preset="xhs-note-cover-landscape" data-export-size="2560x1440" data-export-ratio="16:9">
|
||||
<section data-platform="xhs" data-preset="xhs-video-cover-portrait" data-export-size="1080x1440" data-export-ratio="3:4">
|
||||
```
|
||||
|
||||
#### Douyin / TikTok China
|
||||
|
||||
| Preset | Size | Ratio | Use |
|
||||
|---|---:|---:|---|
|
||||
| `douyin-avatar` | 400 × 400 | 1:1 | 头像 |
|
||||
| `douyin-profile-background` | 1125 × 633 | ~16:9 | 个人背景图 |
|
||||
| `douyin-cover-portrait-3x4` | 1242 × 1660 | ~3:4 | 封面,竖版 |
|
||||
| `douyin-cover-portrait-9x16` | 1080 × 1920 | 9:16 | 封面,竖版 |
|
||||
| `douyin-cover-landscape-16x9` | 1080 × 608 | ~16:9 | 封面,横版 |
|
||||
|
||||
Recommended markers:
|
||||
|
||||
```html
|
||||
<section data-platform="douyin" data-preset="douyin-cover-portrait-9x16" data-export-size="1080x1920" data-export-ratio="9:16">
|
||||
<section data-platform="douyin" data-preset="douyin-cover-landscape-16x9" data-export-size="1080x608" data-export-ratio="16:9">
|
||||
```
|
||||
|
||||
#### WeChat ecosystem
|
||||
|
||||
| Preset | Size | Ratio | Use |
|
||||
|---|---:|---:|---|
|
||||
| `wechat-official-account-avatar` | 240 × 240 | 1:1 | 公众号头像 |
|
||||
| `wechat-official-account-cover` | 900 × 383 | ~2.35:1 | 公众号封面 |
|
||||
| `wechat-official-account-thumbnail` | 200 × 200 | 1:1 | 公众号小图 |
|
||||
| `wechat-official-account-qrcode-card` | 600 × 600 | 1:1 | 公众号二维码名片 |
|
||||
| `wechat-official-account-guide-image` | 1080 × 300 | 18:5 | 公众号内容引导图 |
|
||||
| `wechat-channels-cover-portrait` | 1080 × 1260 | 6:7 | 视频号封面,竖版 |
|
||||
| `wechat-channels-cover-landscape` | 1080 × 608 | ~16:9 | 视频号封面,横版 |
|
||||
| `wechat-miniprogram-cover` | 520 × 416 | 5:4 | 小程序封面 |
|
||||
| `wechat-moments-cover` | 1280 × 1184 | ~1.08:1 | 朋友圈封面 |
|
||||
|
||||
Recommended markers:
|
||||
|
||||
```html
|
||||
<section data-platform="wechat" data-preset="wechat-official-account-cover" data-export-size="900x383">
|
||||
<section data-platform="wechat" data-preset="wechat-official-account-guide-image" data-export-size="1080x300">
|
||||
<section data-platform="wechat-channels" data-preset="wechat-channels-cover-portrait" data-export-size="1080x1260" data-export-ratio="6:7">
|
||||
<section data-platform="wechat-moments" data-preset="wechat-moments-cover" data-export-size="1280x1184">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Default viewport presets
|
||||
|
||||
```text
|
||||
iOS default: 390 × 844
|
||||
iOS large: 430 × 932
|
||||
iOS small: 375 × 812
|
||||
|
||||
Android default: 360 × 800
|
||||
Android large: 412 × 915
|
||||
|
||||
Mobile web / H5: 390 × 844
|
||||
Tablet: 768 × 1024
|
||||
Desktop web: 1440 × 900
|
||||
Dashboard: 1440 × 1024
|
||||
Deck: 1600 × 900
|
||||
Deck HD: 1920 × 1080
|
||||
|
||||
Social square: 1080 × 1080
|
||||
Social portrait: 1080 × 1350
|
||||
Story / vertical: 1080 × 1920
|
||||
Xiaohongshu cover: 1242 × 1660, 1080 × 1080, 2560 × 1440
|
||||
Douyin cover: 1242 × 1660, 1080 × 1920, 1080 × 608
|
||||
WeChat OA cover: 900 × 383
|
||||
WeChat guide: 1080 × 300
|
||||
WeChat Channels: 1080 × 1260 or 1080 × 608
|
||||
Long image: width 1080, height content-driven
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scenario defaults
|
||||
|
||||
### Prototype
|
||||
|
||||
- Mobile app and mobile web previews default to `390 × 844`.
|
||||
- Android-specific previews default to `360 × 800`.
|
||||
- Desktop web previews default to `1440 × 900`.
|
||||
- Dashboard previews default to `1440 × 1024`.
|
||||
- Flow diagrams may use a wide canvas.
|
||||
- Screen previews inside flow diagrams must keep device aspect ratio.
|
||||
|
||||
### Tool H5
|
||||
|
||||
- Canonical surface is mobile portrait.
|
||||
- Default mobile viewport: `390 × 844`.
|
||||
- Desktop view should either center a realistic mobile frame or intentionally expand the experience.
|
||||
- Do not leave a mobile column floating in a large empty desktop canvas.
|
||||
- Result/share cards should use explicit export ratios.
|
||||
|
||||
### Content Page
|
||||
|
||||
- Long-form HTML is responsive.
|
||||
- WeChat article layout is mobile-first and editor-safe.
|
||||
- Generic long image uses explicit width and content-driven height when requested.
|
||||
- Xiaohongshu covers, notes, and fixed-card sequences use `social-card.md`, not `content-page.md`.
|
||||
|
||||
### Deck
|
||||
|
||||
- Default deck canvas: `1600 × 900`.
|
||||
- HD deck export: `1920 × 1080`.
|
||||
- Deck viewport should scale to fit the browser, not reflow like a webpage.
|
||||
|
||||
### Landing Page
|
||||
|
||||
- Responsive web first.
|
||||
- Validate at `375px`, `768px`, and `1280px`.
|
||||
- Product screenshots or device mockups must preserve their own viewport ratios.
|
||||
|
||||
---
|
||||
|
||||
## Device frame rules
|
||||
|
||||
When showing mobile screens:
|
||||
|
||||
- preserve aspect ratio
|
||||
- keep safe margins
|
||||
- include status/nav bars only when useful
|
||||
- keep tap targets readable
|
||||
- avoid shrinking full screens until text becomes unreadable
|
||||
- use simplified mini-previews when space is limited
|
||||
- use exact-ratio full previews in screen inventory, prototype, or tool preview sections
|
||||
|
||||
Recommended markers:
|
||||
|
||||
```html
|
||||
<section data-device="ios" data-viewport="390x844">
|
||||
<section data-device="ios-large" data-viewport="430x932">
|
||||
<section data-device="android" data-viewport="360x800">
|
||||
<section data-platform="mobile-web" data-viewport="390x844">
|
||||
<section data-platform="desktop-web" data-viewport="1440x900">
|
||||
<section data-platform="dashboard" data-viewport="1440x1024">
|
||||
<section data-export-size="1080x1920">
|
||||
<section data-export-ratio="3:4">
|
||||
```
|
||||
|
||||
Recommended CSS:
|
||||
|
||||
```css
|
||||
.phone-frame {
|
||||
width: min(390px, 100%);
|
||||
aspect-ratio: 390 / 844;
|
||||
}
|
||||
|
||||
.phone-frame[data-size="ios-large"] {
|
||||
width: min(430px, 100%);
|
||||
aspect-ratio: 430 / 932;
|
||||
}
|
||||
|
||||
.phone-frame[data-size="ios-small"] {
|
||||
width: min(375px, 100%);
|
||||
aspect-ratio: 375 / 812;
|
||||
}
|
||||
|
||||
.phone-frame[data-size="android"] {
|
||||
width: min(360px, 100%);
|
||||
aspect-ratio: 360 / 800;
|
||||
}
|
||||
|
||||
.desktop-frame {
|
||||
aspect-ratio: 1440 / 900;
|
||||
}
|
||||
|
||||
.dashboard-frame {
|
||||
aspect-ratio: 1440 / 1024;
|
||||
}
|
||||
|
||||
.social-card[data-ratio="3:4"] {
|
||||
aspect-ratio: 3 / 4;
|
||||
}
|
||||
|
||||
.social-card[data-ratio="4:5"] {
|
||||
aspect-ratio: 4 / 5;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Flow diagram vs screen preview
|
||||
|
||||
For interaction flow diagrams:
|
||||
|
||||
- Use a wide canvas for the overall diagram.
|
||||
- Use abstract nodes for minor actions and state changes.
|
||||
- Use exact-ratio screen previews only for key screens.
|
||||
- Do not draw every state as a full phone screen.
|
||||
- Do not let phone previews become landscape cards.
|
||||
- When the screen preview is too large for the flow diagram, use a compact node plus a separate exact-ratio screen inventory.
|
||||
|
||||
---
|
||||
|
||||
## Export surface rules
|
||||
|
||||
When the artifact is meant to be exported as an image:
|
||||
|
||||
- declare the export size or ratio explicitly
|
||||
- keep important content inside safe margins
|
||||
- test title readability at thumbnail size
|
||||
- avoid tiny captions unless export resolution is high enough
|
||||
- do not rely on browser viewport size for fixed social exports
|
||||
- do not silently redesign during export
|
||||
|
||||
For long images:
|
||||
|
||||
- use stable card boundaries
|
||||
- avoid content blocks that depend on viewport height
|
||||
- keep vertical rhythm consistent
|
||||
- set a clear export width, usually `1080px`
|
||||
|
||||
---
|
||||
|
||||
## Quality checklist
|
||||
|
||||
Before delivery, check:
|
||||
|
||||
- Is the artifact canvas appropriate for the scenario?
|
||||
- Are device previews using realistic viewport ratios?
|
||||
- Are mobile screens locked to phone proportions instead of arbitrary cards?
|
||||
- Is the export surface declared when image export is expected?
|
||||
- Does desktop fallback look intentional, not empty?
|
||||
- Are phone previews readable at their displayed size?
|
||||
- Are flow diagrams using abstract nodes when full device previews would hurt readability?
|
||||
Executable
+388
@@ -0,0 +1,388 @@
|
||||
---
|
||||
name: Content Page Skill
|
||||
description: Design publishing-ready HTML content pages for existing articles, reports, newsletters, guides, case studies, release notes, public-account articles, and editorial reading experiences.
|
||||
mode: artifact
|
||||
platform: responsive-web
|
||||
scenario: content-page
|
||||
preview:
|
||||
type: html
|
||||
default_for:
|
||||
- article page
|
||||
- editorial page
|
||||
- newsletter
|
||||
- memo
|
||||
- WeChat article
|
||||
- public account article
|
||||
- long-form content page
|
||||
- report page
|
||||
- whitepaper
|
||||
- guide
|
||||
- tutorial article
|
||||
- case study
|
||||
- release note
|
||||
- changelog article
|
||||
- reading page
|
||||
- 内容页
|
||||
- 文章页
|
||||
- 长文页面
|
||||
- 公众号文章
|
||||
- 公众号长文
|
||||
- 报告页面
|
||||
- 白皮书页面
|
||||
- 案例拆解
|
||||
- 发布说明
|
||||
fidelity: high
|
||||
---
|
||||
|
||||
# Content Page Skill
|
||||
|
||||
Design an editorial reading page for content that already exists or is partially provided.
|
||||
|
||||
This skill is closer to **editorial layout / graphic design** than product UI. Its job is to turn existing content into a readable, publishable, well-paced HTML page through hierarchy, rhythm, typography, evidence treatment, and layout — not to invent the article.
|
||||
|
||||
## Route elsewhere if
|
||||
|
||||
- the primary goal is conversion / signup / sales → `landing-page.md`
|
||||
- the user wants exploration, filters, animated data, or interactive explanation as the main experience → `info-interactive.md`
|
||||
- the user wants a single-task calculator / generator / checker → `web-tool.md`
|
||||
- the user wants a product interface, dashboard, or app flow → `prototype.md`
|
||||
- the user wants a slide presentation → `deck.md`
|
||||
- the final medium is social images, covers, long-image cards, or product brief images → `social-card.md`
|
||||
|
||||
If a content page also needs a social cover, route the reading page here and the cover/card to `social-card.md`; do not force both into one artifact.
|
||||
|
||||
---
|
||||
|
||||
## Core Principle: Content Is Source of Truth
|
||||
|
||||
Assume the user has content unless they explicitly ask for writing.
|
||||
|
||||
Do not invent claims, examples, statistics, quotes, sections, conclusions, sources, charts, or case details to make the page feel fuller.
|
||||
|
||||
The work is to transform available content into a designed publishing artifact:
|
||||
|
||||
- clarify hierarchy;
|
||||
- group related material;
|
||||
- surface key points;
|
||||
- create reading rhythm;
|
||||
- design evidence, figures, quotes, notes, and references;
|
||||
- preserve factual meaning;
|
||||
- mark missing material as placeholder instead of fabricating it.
|
||||
|
||||
If the content is too thin, design a clean structure and ask for the missing material. Do not pad with generic paragraphs.
|
||||
|
||||
---
|
||||
|
||||
## What Makes a Content Page Succeed
|
||||
|
||||
A content page succeeds when the reader can enter quickly, stay oriented, and finish with a clear takeaway.
|
||||
|
||||
The failure mode is not ugliness. The failure mode is a page that is technically readable but editorially flat: title, subtitle, blocks of text, a few cards, no rhythm, no reason to keep reading.
|
||||
|
||||
A strong content page has:
|
||||
|
||||
- **a clear reading promise** — why this is worth reading now;
|
||||
- **a visible editorial structure** — the reader can tell where they are and what kind of section they are in;
|
||||
- **controlled text measure** — body text never stretches across the screen;
|
||||
- **rhythm** — fast scan zones, slow reading zones, evidence breaks, and pauses;
|
||||
- **evidence treatment** — quotes, data, figures, sources, and examples are designed, not dumped;
|
||||
- **a deliberate ending** — conclusion, implication, action, appendix, or related reading.
|
||||
|
||||
---
|
||||
|
||||
## Content Type Detection
|
||||
|
||||
Before designing, infer the content type from the provided material. Do not force every content page into the same article layout.
|
||||
|
||||
| Content type | Use when | Layout direction |
|
||||
|---|---|---|
|
||||
| Article / Essay | one argument, opinion, explanation, or reflection | strong title, readable body, section breaks, pull quotes |
|
||||
| Editorial Feature | narrative, culture, brand story, interview, field notes | immersive opening, image/quote rhythm, slower pace |
|
||||
| Report / Whitepaper | findings, research, industry analysis, structured sections | executive summary, section index, data cards, figures, source notes |
|
||||
| Newsletter / Memo | update, briefing, internal/external note | compact hierarchy, skimmable blocks, clear next step |
|
||||
| Release Note / Changelog Article | product updates, version announcement, feature summary | grouped updates, before/after, screenshots, migration/action notes |
|
||||
| Guide / Resource | how-to, checklist, tutorial, reference material | steps, examples, callouts, warnings, checklist ending |
|
||||
| Case Study | background, challenge, solution, result, reflection | case spine, evidence blocks, before/after, outcome caveats |
|
||||
| Public Account / WeChat Article | mobile-first Chinese reading | single-column rhythm, strong title breaking, paragraph breathing, image captions |
|
||||
|
||||
If the content combines multiple types, choose the dominant one and borrow only the necessary secondary devices.
|
||||
|
||||
---
|
||||
|
||||
## Editorial Layout Principles
|
||||
|
||||
### 1. Start with the text block
|
||||
|
||||
Design the reading column before designing decoration.
|
||||
|
||||
- Body text must have a controlled measure — but bounded on *both* sides. Target roughly **30–45 Chinese characters / 60–75 Latin characters per line**, i.e. a reading column around **wide: clamp(0, ~42em, ~760px)**. Don't run full-width across a desktop viewport, and equally don't over-narrow it into a cramped ~480px ribbon with huge empty margins. If the layout has no side rail/figures, the column can sit toward the upper end (~46em); a too-narrow column is as much a failure as a full-width one.
|
||||
- Long-form text should have generous line-height and stable paragraph rhythm.
|
||||
- Margins should frame and pace the reading, not just center a thin column in a sea of white.
|
||||
- Do not use the whole viewport width just because HTML allows it — but the fix is a *bounded* measure, not an arbitrarily narrow one.
|
||||
|
||||
### 2. Hierarchy is not only font size
|
||||
|
||||
Build hierarchy through a system:
|
||||
|
||||
- title, deck/subtitle, metadata, intro, section heading, subheading, body, quote, note, caption, source;
|
||||
- section spacing and white space;
|
||||
- rules, dividers, labels, side notes, figure captions, callout blocks;
|
||||
- contrast between scan areas and slow-reading areas.
|
||||
|
||||
Avoid the generic hierarchy of “large H1 + medium H2 + plain paragraphs” with no editorial devices.
|
||||
|
||||
A page that is only title + paragraphs + a few cards is the flat failure mode, even if it reads fine. Commit to visible rhythm: alternate dense reading zones with lighter pause zones (quote, figure, data insert, divider), and pick at least one editorial device below so the page has a memory point — don't ship an even, paragraph-after-paragraph wall. For Chinese content specifically, actively use the **表现力手法** in `horizontal-craft/chinese-typography.md` (oversized title contrast, weight contrast, drop-cap/opening ritual, vertical-in-horizontal title/quote, pulled-out 金句 blocks) so the typography itself carries character — not just correct, but expressive. Keep it in service of reading, not decoration.
|
||||
|
||||
### 3. Choose one editorial device
|
||||
|
||||
Use one primary device to give the page memory. Do not use all devices at once.
|
||||
|
||||
Good devices:
|
||||
|
||||
- pull quote;
|
||||
- side note / margin note;
|
||||
- evidence card;
|
||||
- source appendix;
|
||||
- timeline;
|
||||
- figure strip;
|
||||
- glossary;
|
||||
- case card;
|
||||
- data insert;
|
||||
- chapter index;
|
||||
- key-takeaway rail.
|
||||
|
||||
The device must come from the content. Do not add a timeline if there is no sequence; do not add a data card if there is no real data.
|
||||
|
||||
### 4. Figures and images must carry meaning
|
||||
|
||||
Images, charts, diagrams, and screenshots are not filler.
|
||||
|
||||
- Every figure needs a reason and usually a caption.
|
||||
- Screenshots should be legible and framed deliberately.
|
||||
- Data visualizations need title, unit, label, and source/placeholder status.
|
||||
- Avoid generic stock imagery that does not add evidence, atmosphere, or context.
|
||||
|
||||
### 5. Ending is part of the design
|
||||
|
||||
Do not finish every page with a generic CTA.
|
||||
|
||||
Choose the ending based on content:
|
||||
|
||||
- conclusion;
|
||||
- implication;
|
||||
- next action;
|
||||
- open question;
|
||||
- checklist;
|
||||
- source appendix;
|
||||
- related reading;
|
||||
- author note;
|
||||
- changelog / version note.
|
||||
|
||||
---
|
||||
|
||||
## Reading Rhythm
|
||||
|
||||
Design where the reader speeds up and where they slow down.
|
||||
|
||||
A good content page usually contains:
|
||||
|
||||
1. **Entry** — title, subtitle/deck, metadata, and reading promise.
|
||||
2. **Orientation** — summary, table of contents, key finding, or context block.
|
||||
3. **Body rhythm** — sections with varied density, not one long wall of text.
|
||||
4. **Pause points** — quote, figure, note, data insert, example, or divider.
|
||||
5. **Evidence zones** — sources, charts, examples, screenshots, case blocks.
|
||||
6. **Transition cues** — section breaks that tell the reader what changed.
|
||||
7. **Ending beat** — final takeaway, implication, or appendix.
|
||||
|
||||
Opening moves should be extracted from the content, not invented:
|
||||
|
||||
- thesis first;
|
||||
- scene first;
|
||||
- data first;
|
||||
- question first;
|
||||
- quote first;
|
||||
- object/screenshot first;
|
||||
- context first.
|
||||
|
||||
If no strong opening is evident, use a clear thesis/context opening rather than fabricating drama.
|
||||
|
||||
---
|
||||
|
||||
## Layout Models
|
||||
|
||||
Choose one model and commit.
|
||||
|
||||
### Classic Reading Page
|
||||
|
||||
Use for essays, explanations, opinion pieces, and public-account style articles.
|
||||
|
||||
- single strong text column;
|
||||
- restrained metadata;
|
||||
- clear section headings;
|
||||
- occasional pull quote or figure;
|
||||
- calm ending.
|
||||
|
||||
### Editorial Feature
|
||||
|
||||
Use for narrative, culture, brand story, interview, or field notes.
|
||||
|
||||
- stronger opening treatment;
|
||||
- image / quote / text rhythm;
|
||||
- more atmospheric spacing;
|
||||
- slower reading pace.
|
||||
|
||||
### Report / Dossier
|
||||
|
||||
Use for structured analysis, whitepapers, industry reports, or research summaries.
|
||||
|
||||
- executive summary;
|
||||
- section index;
|
||||
- data cards / figure blocks;
|
||||
- source notes;
|
||||
- appendix or methodology area when relevant.
|
||||
|
||||
### Newsletter / Brief
|
||||
|
||||
Use for updates, memos, weekly notes, and product briefings that are meant to be scanned.
|
||||
|
||||
- compact intro;
|
||||
- labeled sections;
|
||||
- clear “what changed / why it matters / next step” structure;
|
||||
- lighter visuals.
|
||||
|
||||
### Guide / Resource
|
||||
|
||||
Use for tutorials, how-tos, references, checklists, or resource pages.
|
||||
|
||||
- step blocks;
|
||||
- examples;
|
||||
- warnings / tips;
|
||||
- final checklist;
|
||||
- stable navigation if long.
|
||||
|
||||
### Public Account / Mobile Article
|
||||
|
||||
Use for Chinese mobile reading and WeChat-like publishing.
|
||||
|
||||
- single-column mobile-first body;
|
||||
- strong title line breaks;
|
||||
- short paragraph rhythm;
|
||||
- image captions and section dividers;
|
||||
- avoid dense desktop-style sidebars.
|
||||
|
||||
---
|
||||
|
||||
## Typography And Grid
|
||||
|
||||
For Chinese content, apply `horizontal-craft/chinese-typography.md`.
|
||||
|
||||
Escalate to deep references when needed:
|
||||
|
||||
- `horizontal-craft/reference/text-detail.md` — body size, line-height, paragraph rhythm, long-form readability.
|
||||
- `horizontal-craft/reference/title-and-breaking.md` — headlines, decks, section titles, Chinese line breaks.
|
||||
- `horizontal-craft/reference/punctuation.md` — punctuation-heavy Chinese, quotations, lists, notes, captions.
|
||||
- `horizontal-craft/reference/grid-system.md` — report, dossier, multi-column, long image, or complex editorial layout.
|
||||
- `horizontal-craft/reference/fonts.md` — only when font choice materially affects the editorial tone, e.g. literary, academic, public-document, magazine, poster-like title, or brand-specific typography.
|
||||
|
||||
Do not read `fonts.md` for every Chinese article. Most web content can use a safe system font stack unless the typography itself is part of the visual direction.
|
||||
|
||||
---
|
||||
|
||||
## HTML Structure
|
||||
|
||||
Keep the page editable and semantically clear.
|
||||
|
||||
Recommended markers:
|
||||
|
||||
```html
|
||||
<main data-content-page="topic-name" data-content-type="report">
|
||||
<header data-content-section="entry">…</header>
|
||||
<section data-content-section="summary">…</section>
|
||||
<section data-content-section="chapter" id="chapter-1">…</section>
|
||||
<aside data-editorial-device="pull-quote">…</aside>
|
||||
<figure data-figure="chart" data-source-status="provided|placeholder">…</figure>
|
||||
<section data-content-section="appendix">…</section>
|
||||
</main>
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- preserve real content structure;
|
||||
- keep headings semantic;
|
||||
- mark placeholder areas clearly;
|
||||
- keep figures, captions, notes, sources, and callouts identifiable;
|
||||
- avoid opaque generated blobs that make selected-region edits impossible.
|
||||
|
||||
---
|
||||
|
||||
## References, Load Only When Triggered
|
||||
|
||||
Core reading/layout references:
|
||||
|
||||
- `horizontal-craft/chinese-typography.md` → substantial Chinese text.
|
||||
- `horizontal-craft/reference/text-detail.md` → long-form reading, dense body copy, report/article rhythm.
|
||||
- `horizontal-craft/reference/title-and-breaking.md` → strong title, Chinese headline, public-account title, editorial section headings.
|
||||
- `horizontal-craft/reference/punctuation.md` → punctuation-heavy Chinese, quotes, citations, notes.
|
||||
- `horizontal-craft/reference/grid-system.md` → report/dossier/multi-column/long-image layout.
|
||||
- `horizontal-craft/reference/fonts.md` → font choice materially affects tone.
|
||||
|
||||
Supporting references:
|
||||
|
||||
- `horizontal-craft/color.md` → no design system or color direction is vague.
|
||||
- `horizontal-craft/data-integrity.md` → data, charts, metrics, rankings, percentages, research claims.
|
||||
- `horizontal-craft/link-and-proof.md` → citations, links, client names, testimonials, sources, proof claims.
|
||||
- `horizontal-craft/visual-explanation.md` → diagrams, timelines, maps, process figures, comparison graphics, explanatory charts.
|
||||
- `horizontal-craft/icon-system.md` → icons, pictograms, labels, repeated symbolic marks.
|
||||
- `design-system-reference.md` → brand/style/template/DESIGN.md reference exists.
|
||||
|
||||
Normally avoid:
|
||||
|
||||
- `state-coverage.md` unless the page contains meaningful interactive states.
|
||||
- `form-validation.md` unless there are forms.
|
||||
- `animation-discipline.md` unless motion is requested or used.
|
||||
- `technique-library.md` unless advanced visualization or interaction is truly necessary.
|
||||
|
||||
---
|
||||
|
||||
## Decision Sequence
|
||||
|
||||
1. Inventory the provided content: title, sections, claims, evidence, figures, sources, missing parts.
|
||||
2. Extract the editorial stance from the content: explainer, report, memo, guide, case, release note, essay, public-account article, etc.
|
||||
3. Choose the reading promise: what should the reader understand or take away?
|
||||
4. Select one layout model and one editorial device.
|
||||
5. Design the text block, hierarchy, rhythm, figures, and ending.
|
||||
6. Build the HTML while preserving content truth and marking placeholders.
|
||||
7. Run the final gate.
|
||||
|
||||
---
|
||||
|
||||
## Final Gate
|
||||
|
||||
Run `quality-gate.md` as the shared blocking check. This skill adds content-page-specific checks:
|
||||
|
||||
- content meaning preserved; no invented claims, examples, quotes, data, or conclusions;
|
||||
- placeholder material clearly marked;
|
||||
- body text measure and line-height support real reading;
|
||||
- hierarchy uses more than font size alone;
|
||||
- long pages have visible section rhythm and orientation;
|
||||
- figures/images/charts have a reason, caption, and source/placeholder status where relevant;
|
||||
- links, citations, and proof claims are real or clearly marked;
|
||||
- Chinese text applies `horizontal-craft/chinese-typography.md` when substantial;
|
||||
- the page does not look like a landing page, slide deck, or social card unless explicitly hybrid;
|
||||
- the ending matches the content instead of defaulting to a generic CTA.
|
||||
|
||||
---
|
||||
|
||||
## Handoff
|
||||
|
||||
Use the standard Design Skill delivery, export, and version-management flow.
|
||||
|
||||
If requested, define named export targets for:
|
||||
|
||||
- PDF / print-style article;
|
||||
- WeChat/public-account publishing package;
|
||||
- long image export;
|
||||
- source appendix;
|
||||
- downloadable report assets.
|
||||
|
||||
If the user also needs a social cover, carousel, or summary image, route that output to `social-card.md` rather than forcing it into the reading page.
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
---
|
||||
name: Deck Skill
|
||||
description: Design HTML-based slide decks, presentation decks, pitch decks, report decks, and narrative visual presentations.
|
||||
mode: deck
|
||||
platform: presentation
|
||||
scenario: deck
|
||||
preview:
|
||||
type: html
|
||||
default_for:
|
||||
- deck
|
||||
- presentation
|
||||
- slide deck
|
||||
- pitch deck
|
||||
- product deck
|
||||
- report deck
|
||||
- strategy deck
|
||||
- workshop deck
|
||||
- 演示文稿
|
||||
- 路演材料
|
||||
speaker_notes: optional
|
||||
animations: optional
|
||||
fidelity: high
|
||||
---
|
||||
|
||||
# Deck Skill
|
||||
|
||||
Design a paged, one-screen-at-a-time presentation that carries a narrative — each slide makes
|
||||
one point, and the sequence builds an argument.
|
||||
|
||||
Judgment, not a rulebook. Use taste. Techniques: `horizontal-craft/technique-library.md`; delivery
|
||||
check: `skills/design/quality-gate.md`. For a base runtime/template, see
|
||||
`skills/design/prototype-templates/` (html-ppt seed) and the Preset Template Rule in SKILL.md.
|
||||
|
||||
## Route elsewhere if
|
||||
|
||||
- it's a scrollable web page (not paged) → `landing-page.md` / `content-page.md`
|
||||
- it's an interactive product → `prototype.md`
|
||||
|
||||
## What makes a deck succeed: one idea per slide, a story across slides
|
||||
|
||||
The failure mode is text-dumped slides with no narrative spine — bullet walls that should have
|
||||
been a doc. So:
|
||||
|
||||
- **One idea per slide.** Each slide has a single takeaway, stated clearly. If a slide needs two
|
||||
points, it's two slides.
|
||||
- **A narrative arc.** The deck has a beginning (problem/context), middle (argument/evidence),
|
||||
and end (ask/conclusion). Order the slides to build, not just enumerate.
|
||||
- **Show, don't dump.** Visualize relationships and data; a sentence + a strong visual beats a
|
||||
paragraph. Speaker notes carry the detail, not the slide.
|
||||
|
||||
## The few decisions that matter
|
||||
|
||||
- **Deck type & tone** — pitch (persuasive, bold), report (clear, sober), strategy (structured),
|
||||
workshop (interactive). This sets density, color, and pacing.
|
||||
- **Narrative spine** — the sequence of takeaways before any visuals. Get the story right first.
|
||||
- **Slide rhythm** — vary slide types (statement, data, comparison, quote, section break);
|
||||
don't repeat the same layout 20 times. Use full-bleed moments for emphasis.
|
||||
- **Data honesty** — charts with correct axes/units/scales; the title states the insight; never
|
||||
invent numbers.
|
||||
|
||||
## Non-negotiables
|
||||
|
||||
- One idea per slide; speaker-facing detail goes in notes, not on the slide.
|
||||
- Never invent data, quotes, or metrics; charts faithful to real numbers.
|
||||
- Consistent system (type scale, color, spacing) across all slides.
|
||||
- No decorative emoji as icons (use SVG); no Lorem ipsum.
|
||||
|
||||
## Avoiding AI slop (one principle)
|
||||
|
||||
Catalog in `quality-gate.md`. Principle: each slide's layout serves its specific point, not a
|
||||
repeated template. A title + three bullets on every slide, a generic gradient title slide,
|
||||
identical icon-feature rows — reflexes. Vary with intent; when a slide feels like filler, cut it.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Define the deck type, audience, and the single ask/conclusion.
|
||||
2. Write the narrative spine (the ordered takeaways) before designing slides.
|
||||
3. Design slides — one idea each, varied rhythm, visuals over text; detail to speaker notes.
|
||||
4. Techniques/animations from `horizontal-craft/technique-library.md` only when they aid the point; reduced-motion.
|
||||
5. Quality gate before delivery; verify data honesty and one-idea-per-slide.
|
||||
|
||||
## References (load only when relevant)
|
||||
|
||||
- `horizontal-craft/chinese-typography.md` — substantial Chinese text (Mandatory Runtime Baseline).
|
||||
- `horizontal-craft/icon-system.md` — when icons are used.
|
||||
- `canvas-and-device.md` — slide dimensions / export sizes.
|
||||
- `design-system-reference.md` — when a brand/style/template reference is given (see SKILL.md Style Routing).
|
||||
- `horizontal-craft/technique-library.md` — restrained transitions only when they aid the narrative.
|
||||
|
||||
## Creative Context
|
||||
|
||||
For open-ended requests, complete `SKILL.md` Creative Context (creative_positioning,
|
||||
first_impression, anti_default) before generating.
|
||||
|
||||
## Mandatory Final Gate
|
||||
|
||||
Run `quality-gate.md` as a blocking check. Must satisfy: one idea per slide; honest data/charts;
|
||||
consistent system across slides; no invented numbers/quotes; no emoji-as-icons / Lorem ipsum;
|
||||
visible focus and reduced-motion where interactive; non-visible Design Compliance block matching
|
||||
the code. Apply `horizontal-craft/chinese-typography.md` if substantial Chinese text.
|
||||
|
||||
## Handoff
|
||||
|
||||
May continue to: `design-system-reference.md`, `design-system-generation.md`, `export.md`,
|
||||
`skills/version-management/SKILL.md`, built-in selected-region edits, and local parameter handling.
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
---
|
||||
name: Design System Generation Skill
|
||||
description: Import, extract, normalize, or persist a reusable DESIGN.md from an existing design system, HTML artifact, screenshot, template, style reference, or generated interface.
|
||||
---
|
||||
|
||||
# Design System Generation Skill
|
||||
|
||||
把视觉规则变成一份可复用的 `DESIGN.md`。普通首版页面生成**不需要**这个 skill;仅当用户要导入、提取、规范化、持久化或复用一套设计语言时用。核心理念(借鉴 DESIGN.md 方法):用自然语言描述视觉系统、有精确值就保留、并解释每个设计选择**为什么**存在而不只是长什么样。产出经 `skills/design/design-system-reference.md` 供未来生成复用。
|
||||
|
||||
## 三种模式
|
||||
|
||||
- **Import(导入)**:用户给了现成 `DESIGN.md`/设计规范,要变成可复用参考。行为:读取、保留用户自有规则、检查缺失段、仅在有用时规范化结构、**不要按模型自己的品味重写**、保留精确值、必要时标出歧义。(如"这是我的 DESIGN.md 后面按这个来"、"检查这份有没有缺失")
|
||||
- **Extraction(提取)**:从 HTML/截图/模板/页面/CSS/style skill/品牌参考提取设计规则。行为:检视来源、提取可见值与模式、区分精确值与推断值、**既描述表面风格也描述底层意图**、不机械照抄每条 CSS、不臆造没有的细节、分离可复用规则与一次性细节。(如"从这个 HTML 提炼 DESIGN.md"、"把截图总结成设计系统")
|
||||
- **Persistence(持久化)**:把当前已认可的产物风格沉淀成未来可复用的系统。行为:检视当前产物、从已认可设计提取稳定规则、识别哪些该重复哪些是一次性、**别把页面特有细节变成通用规则**。(如"把当前页面风格沉淀成 DESIGN.md"、"后面页面都沿用这版")
|
||||
|
||||
多模式同时适用时,以用户明确请求为准。
|
||||
|
||||
## 不要用这个 skill 当
|
||||
|
||||
只生成单个首版页面、只用现有设计系统而不改它、只做局部编辑、只改确定性参数(色/间距/圆角/密度)、或普通的设计参考查找(那用 `design-system-reference.md`)。
|
||||
|
||||
---
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. 判断模式
|
||||
分类为 Import / Extraction / Persistence;多个适用时以用户明确请求为准。
|
||||
|
||||
---
|
||||
|
||||
### 2. 提取设计意图(不只描述外观)
|
||||
|
||||
解释设计为什么用每个模式,而不只是它长什么样。
|
||||
|
||||
弱:`蓝色按钮、圆角卡片。`
|
||||
好:`蓝色专用于主操作与进度状态,给界面一条清晰路径;圆角软化了原本偏技术感的布局,让密集信息更易接近。`
|
||||
|
||||
应说清:每个视觉选择传达什么、哪些是结构性哪些是装饰性、如何扩展到未来页面、哪些元素是风格的核心、哪些不该被滥用。
|
||||
|
||||
### 3. 分离稳定规则与一次性选择
|
||||
|
||||
好的设计系统不盲目保留每个细节。把决策分为:**核心规则**(未来都重复)、**可选母题**(有用但非处处必需)、**一次性细节**(只属于当前产物)、**应避免**(不该重复)。
|
||||
|
||||
例:
|
||||
```text
|
||||
核心规则:项目年份/角色/状态用紧凑等宽元数据标签。
|
||||
可选母题:项目多时,列表用 hover 预览图。
|
||||
一次性细节:超大动画项目编号只属于首页 hero。
|
||||
应避免:别给无关内容页加随机系统标签。
|
||||
```
|
||||
|
||||
### 4. 产出或更新 DESIGN.md
|
||||
|
||||
用户给了现成 `DESIGN.md` → 更新/规范化而非整体替换;从产物提取 → 产出新的;持久化当前设计 → 产出供 `design-system-reference.md` 复用的。简洁但足以指导未来生成,别搞庞大的 design bible。
|
||||
|
||||
---
|
||||
|
||||
# DESIGN.md 结构
|
||||
|
||||
默认用下面 9 段结构。每段写**实用规则 + 为什么**(不只是值),有精确值就保留、推断的标注清楚。
|
||||
|
||||
**1. Visual Theme & Atmosphere** — 整体气质:情绪、精致度、密度、视觉温度、个性、受众、头 2 秒的感受。**用一个具体参照物锚定主题,而非一串形容词**——"现代/干净/高级/可信"什么都没指定,模型只会做出落在这些词中心的平庸结果;形容词描述一个「区域」,一个具体参照(某类实物/场景/年代,如"1970 年代某老牌大学的研究生讲义")描述一个「点」,一句话带出配色/字体/留白/有无装饰,还自带「它不是什么」。若一定要用形容词,定义它在本系统里的具体含义。
|
||||
|
||||
**2. Color Palette & Roles** — 按**角色**而非清单记录颜色,说明每个用在哪、哪些不该滥用,强调克制 accent,没可见值不臆造、推断的标注:
|
||||
```md
|
||||
### Core Palette
|
||||
- Background `#F7F3EA` — 暖调编辑感底色,页面背景。
|
||||
- Text Primary `#171717` — 高对比正文与标题。
|
||||
- Text Muted `#73706A` — 元数据、说明、次要细节。
|
||||
- Accent `#D94F30` — 仅用于主 CTA、激活态、每区块一处高亮。
|
||||
- Border `rgba(23,23,23,0.14)` — 不靠重盒子的细微结构。
|
||||
```
|
||||
|
||||
**3. Typography Rules** — 字族 + **语义层级体系**(多数系统 9–15 级,用 headline/display/body/label/caption 等语义命名,每类可再分 small/medium/large),各级字号/字重/行高、层级跨度(克制:标题约 1.9× 正文而非 5×)、中-拉-数字混排。
|
||||
|
||||
**4. Layout & Elevation(布局与层级)** — 网格/版心、间距节奏、留白策略、内容区结构、对齐逻辑(结构而非装饰)。**并说清"层级/深度怎么表达"**:用阴影就定义阴影规则;扁平设计要说清替代方案(边框 / 色彩对比 / 色调分层,如背景用浅色、主内容用纯白卡)——层级如何传达是核心决策,不能漏。
|
||||
|
||||
**5. Component & Shape(组件与形状)** — **形状语言**:整体的形状性格(锐利 / 柔和)与统一的圆角策略(如所有交互元素统一 4px,"克制的现代感")。**组件形态**:关键组件(卡片/按钮/标签/输入框等)的圆角、边框、阴影、内距规则,**尤其覆盖状态变体**(primary/secondary、hover/pressed/active、空/错误/禁用)——状态覆盖是设计系统的关键,不只画默认态。
|
||||
|
||||
**6. Interaction & Motion** — 动效性格、入场/hover/过渡惯例、始终带 reduced-motion,区分核心交互与一次性效果。
|
||||
|
||||
**7. Responsive Behavior** — 断点、移动端如何重排、触控友好、各视口可读性。
|
||||
|
||||
**8. Do's and Don'ts** — 写具体规则。**注意:又长又啰嗦的 Don'ts 往往说明主题描述太模糊(足够具体的参照会自带大部分负向约束);一个具体参照 + 一份刻意的 Do/Don't 才是最佳组合。**
|
||||
```text
|
||||
Do:作品为主要佐证;保持强层级;元数据标签克制一致;限制 accent 数量;用一个难忘的结构/交互主意;有精确值就复用。
|
||||
Don't:假评价/假指标/假 logo;通用区块填空;照抄品牌素材/专有 UI;无目的的装饰渐变/emoji;每个区块一样响;可发布产物里有死链/失效点击;把一次性 hero 效果当通用规则。
|
||||
```
|
||||
|
||||
**9. Agent Prompt Guide** — 写给未来 agent 的、直接可操作的生成指令:
|
||||
```md
|
||||
在本系统里生成新页面时:从作品优先结构开始;角色/年份/类别用紧凑元数据标签;accent 保留给主操作与激活态;尽早展示真实作品、别先放长 bio;用一个大胆结构动作、其余区块安静;除非用户要简单版避免通用卡片网格;保留响应式与触控友好。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Lightweight Tokens
|
||||
|
||||
If useful, include a compact token section.
|
||||
|
||||
Use only when values are available or can be responsibly inferred.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"color": {
|
||||
"background": "#F7F3EA",
|
||||
"surface": "#FFFFFF",
|
||||
"textPrimary": "#171717",
|
||||
"textMuted": "#73706A",
|
||||
"accent": "#D94F30",
|
||||
"border": "rgba(23, 23, 23, 0.14)"
|
||||
},
|
||||
"radius": {
|
||||
"sm": "6px",
|
||||
"md": "12px",
|
||||
"lg": "24px"
|
||||
},
|
||||
"spacing": {
|
||||
"base": "8px",
|
||||
"section": "96px"
|
||||
},
|
||||
"typography": {
|
||||
"bodySize": "16px",
|
||||
"bodyLineHeight": "1.5",
|
||||
"labelSize": "11px"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Do not include fake precision. If values are inferred, state that they are inferred.
|
||||
|
||||
---
|
||||
|
||||
## 各模式补充(避免项)
|
||||
|
||||
三模式的核心行为已在前面「三种模式」给出。各自要避免:
|
||||
- **Extraction**:别把原始 CSS 倒进 DESIGN.md、别臆造没有的值、别把一次性细节当系统规则、别漏掉响应式/交互。
|
||||
- **Persistence**:别过拟合当前页、别把占位内容当设计规则、别把临时实验固化成永久系统、别忽略用户已认可的风格决定。
|
||||
- **Import**:输出可以是更新版/规范化版/缺口分析+建议/兼容性说明;不删用户规则除非冲突或重复。
|
||||
|
||||
## Quality Rules
|
||||
|
||||
一份好的 `DESIGN.md`:可复用、具体、扎根源材料、简洁够用、分清核心vs可选、IP安全、对未来生成有用。**避免**:通用设计套话、空泛的 premium/modern/clean 措辞、有理论无可操作规则、照抄专有品牌系统、把单个产物细节当通用规则、臆造源材料不支持的 token。
|
||||
|
||||
## IP and Brand Safety
|
||||
|
||||
源是公开品牌灵感时:不把输出当官方系统、不照抄 logo/素材/精确页面结构/专有 UI、只描述高层视觉特质、产出导向原创的可复用规则。源是用户自有时:明确提供则视为权威、保留精确值、不完整处标注假设。
|
||||
|
||||
## Chinese Typography Reference
|
||||
|
||||
产物含大量中文/CJK、中文编辑排版、公众号、小红书、中文 deck、中文 UI 标签或印刷风 HTML 时,用 `horizontal-craft/chinese-typography.md`;把印刷方法转为 HTML 结构:版心、网格、留白、标题组、图版、边注、章节节奏与标点。
|
||||
|
||||
## Quality Gate
|
||||
|
||||
交付前应用 `quality-gate.md`;本场景可加更严的检查,但不得削弱共享门禁。
|
||||
|
||||
## Handoff
|
||||
|
||||
生成/导入/规范化/更新后的 `DESIGN.md` 经 `skills/design/design-system-reference.md` 供未来设计复用,应是可复用参考而非一次性总结。有用时记下:源产物/模板/截图、所选 style skill/品牌灵感、日期版本、假设、缺失信息。
|
||||
Executable
+600
@@ -0,0 +1,600 @@
|
||||
---
|
||||
name: Design System Reference Skill
|
||||
description: Select and apply provided style skills, brand inspiration references, open-source component systems, preset HTML templates, or user-owned visual systems to guide design generation and edits.
|
||||
mode: reference
|
||||
platform: any
|
||||
scenario: design-system-reference
|
||||
preview:
|
||||
type: none
|
||||
default_for:
|
||||
- style reference
|
||||
- brand inspiration
|
||||
- design system reference
|
||||
- preset template
|
||||
- open-source component system
|
||||
- user-owned design system
|
||||
- DESIGN.md reference
|
||||
fidelity: system
|
||||
---
|
||||
|
||||
# Design System Reference Skill
|
||||
|
||||
Use this skill when a design task should follow an existing reference instead of inventing a new visual language.
|
||||
|
||||
This skill is not a artifact skill. It does not decide whether the artifact is a portfolio, landing page, prototype, content page, or H5 tool.
|
||||
|
||||
It works together with artifact skills by providing visual, structural, component, template, and style constraints.
|
||||
|
||||
Examples:
|
||||
|
||||
- `portfolio.md` + `design-system-reference.md`
|
||||
- `deck.md` + `design-system-reference.md`
|
||||
- `prototype.md` + `design-system-reference.md`
|
||||
- `landing-page.md` + `design-system-reference.md`
|
||||
- `web-tool.md` + `design-system-reference.md`
|
||||
|
||||
---
|
||||
|
||||
## Trigger
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- the user explicitly selects a design system, style, brand reference, or preset HTML template
|
||||
- the user asks to use a known style skill
|
||||
- the user asks to use a known open-source component system
|
||||
- the user mentions a public brand or product style as inspiration
|
||||
- the user uploads or provides a design system, style guide, screenshot, codebase, tokens, or component library
|
||||
- the user asks to keep the new artifact consistent with an existing artifact
|
||||
- the user says to follow a previous version, selected template, UI kit, component style, or visual language
|
||||
|
||||
Examples:
|
||||
|
||||
- “用 Ant Design 做”
|
||||
- “用 shadcn/ui”
|
||||
- “做成 editorial 风格”
|
||||
- “更 minimal”
|
||||
- “像 Apple 一点”
|
||||
- “参考 Linear”
|
||||
- “小红书风格”
|
||||
- “按这个截图的风格”
|
||||
- “沿用上次那个页面的风格”
|
||||
- “这是我们的设计规范,按这个来”
|
||||
- “按这个 HTML 模板改”
|
||||
|
||||
Do not use this skill when:
|
||||
|
||||
- the task has no visual, system, style, or template reference
|
||||
- the user only wants pure writing, research, or coding
|
||||
- the user asks for a new visual direction without any reference
|
||||
- the reference would require copying proprietary assets, logos, distinctive commercial layouts, or private UI patterns
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
The goal is to improve design quality and consistency by grounding output in the right reference.
|
||||
|
||||
This skill should help the agent:
|
||||
|
||||
- avoid generic visual taste
|
||||
- select only the relevant reference
|
||||
- use existing systems when available
|
||||
- distinguish reusable style skills from public brand inspiration
|
||||
- distinguish open-source component systems from proprietary brand UI
|
||||
- prevent unauthorized brand copying
|
||||
- preserve consistency across generated artifacts
|
||||
- reuse templates, tokens, components, and visual patterns responsibly
|
||||
|
||||
The goal is not to force every design into a design system.
|
||||
|
||||
If no relevant reference exists, proceed with scenario-specific defaults from `skills/design/SKILL.md`.
|
||||
|
||||
---
|
||||
|
||||
## Available Reference Libraries
|
||||
|
||||
During testing, the system may provide two reference libraries.
|
||||
|
||||
Look up known references in:
|
||||
|
||||
`skills/design/design-systems/INDEX.md`
|
||||
|
||||
Do not load every reference by default. Select only the most relevant reference.
|
||||
|
||||
---
|
||||
|
||||
### 1. Style Skills
|
||||
|
||||
Path:
|
||||
|
||||
`skills/design/design-systems/style-skills/`
|
||||
|
||||
These are reusable design style skills. Each style may contain:
|
||||
|
||||
- `SKILL.md`
|
||||
- `DESIGN.md`
|
||||
- `components.html`, optional
|
||||
- `tokens.css`, optional
|
||||
|
||||
Use these when the user explicitly requests a visual style, UI style, design mood, or implementation style.
|
||||
|
||||
Examples:
|
||||
|
||||
- minimal
|
||||
- luxury
|
||||
- editorial
|
||||
- paper
|
||||
- retro
|
||||
- dashboard
|
||||
- shadcn
|
||||
- ant
|
||||
- glassmorphism
|
||||
- neobrutalism
|
||||
- professional
|
||||
- expressive
|
||||
|
||||
Behavior:
|
||||
|
||||
- Treat these as reusable design recipes.
|
||||
- Read the selected style's `SKILL.md` and `DESIGN.md` before generating when available.
|
||||
- Apply only the relevant style guidance to the current artifact.
|
||||
- Do not load all style skills.
|
||||
- Do not combine many style skills unless the user explicitly asks.
|
||||
- If multiple style skills match, choose the most specific one.
|
||||
- If the style skill conflicts with the user's content or selected template, preserve the user's goal first.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
“做一个 editorial 风格的个人作品集”
|
||||
→ `portfolio.md`
|
||||
→ `design-system-reference.md`
|
||||
→ `design-systems/style-skills/editorial/`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Brand Inspiration
|
||||
|
||||
Path:
|
||||
|
||||
`skills/design/design-systems/brand-inspiration/`
|
||||
|
||||
These are public brand-inspired `DESIGN.md` references.
|
||||
|
||||
Use these when the user mentions a known public brand or product style.
|
||||
|
||||
Examples:
|
||||
|
||||
- Apple
|
||||
- Stripe
|
||||
- Notion
|
||||
- Airbnb
|
||||
- Vercel
|
||||
- Figma
|
||||
- Nike
|
||||
- Tesla
|
||||
- Shopify
|
||||
- Spotify
|
||||
|
||||
Behavior:
|
||||
|
||||
- Treat these as inspiration only.
|
||||
- Read the selected brand's `DESIGN.md` when available.
|
||||
- Extract high-level qualities such as mood, hierarchy, density, typography direction, spacing rhythm, color restraint, and interaction feel.
|
||||
- Create an original design.
|
||||
- Do not copy logos, layouts, assets, proprietary screens, branded UI, or distinctive commercial patterns.
|
||||
- Do not claim the output uses the official design system.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
“做一个 Apple 风格的作品集”
|
||||
→ `portfolio.md`
|
||||
→ `design-system-reference.md`
|
||||
→ `design-systems/brand-inspiration/apple/`
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reference Types
|
||||
|
||||
There are five reference types.
|
||||
|
||||
Use the strongest applicable type. Do not load all reference types by default.
|
||||
|
||||
---
|
||||
|
||||
### 1. User-Owned Design System
|
||||
|
||||
Use this when the user provides or owns the design system.
|
||||
|
||||
Examples:
|
||||
|
||||
- uploaded brand guideline
|
||||
- uploaded tokens
|
||||
- uploaded UI kit
|
||||
- uploaded component library
|
||||
- existing project codebase
|
||||
- existing product screenshots
|
||||
- user-provided Figma file or future Figma MCP reference
|
||||
- previous artifact created in the same workspace
|
||||
|
||||
Priority:
|
||||
|
||||
Highest.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Treat as authoritative.
|
||||
- Read the provided source before generating.
|
||||
- Prefer exact tokens, colors, typography, spacing, radius, shadows, components, and patterns.
|
||||
- Match the existing visual vocabulary before adding new elements.
|
||||
- Do not invent new visual rules unless the user asks for a new direction.
|
||||
- If the design system is incomplete, fill gaps conservatively and record assumptions.
|
||||
|
||||
Use when the user says:
|
||||
|
||||
- “按我们的设计规范”
|
||||
- “沿用这个项目的风格”
|
||||
- “参考这个页面”
|
||||
- “基于这个截图”
|
||||
- “用这个组件库”
|
||||
- “跟之前那个版本保持一致”
|
||||
|
||||
---
|
||||
|
||||
### 2. Style Skill Reference
|
||||
|
||||
Use this when the user asks for a general visual style, design mood, or UI style that exists in `style-skills/`.
|
||||
|
||||
Examples:
|
||||
|
||||
- minimal
|
||||
- luxury
|
||||
- editorial
|
||||
- paper
|
||||
- retro
|
||||
- dashboard
|
||||
- shadcn
|
||||
- ant
|
||||
- glassmorphism
|
||||
- neobrutalism
|
||||
- professional
|
||||
- expressive
|
||||
|
||||
Priority:
|
||||
|
||||
High when explicitly requested.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Treat as a reusable design recipe.
|
||||
- Read the selected style's `SKILL.md` and `DESIGN.md` when available.
|
||||
- Apply the selected style to the current scenario, not the other way around.
|
||||
- Do not let a style skill override the user's core artifact goal.
|
||||
- Do not combine many style skills by default.
|
||||
|
||||
---
|
||||
|
||||
### 3. Open-Source Component System
|
||||
|
||||
Use this when the user explicitly asks for a public/open-source design system or component library.
|
||||
|
||||
Examples:
|
||||
|
||||
- Ant Design
|
||||
- Material UI / MUI
|
||||
- Chakra UI
|
||||
- Radix UI
|
||||
- shadcn/ui
|
||||
- Bootstrap
|
||||
- Tailwind UI, when license/access permits
|
||||
|
||||
Priority:
|
||||
|
||||
High when explicitly requested.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Use the system directly according to public documentation and license.
|
||||
- Prefer official components, tokens, layout patterns, and usage guidance.
|
||||
- Generate implementation using real component APIs when relevant.
|
||||
- Respect attribution or license requirements when relevant.
|
||||
- Do not confuse the open-source system with the company’s private brand assets or internal product UI.
|
||||
|
||||
Use when the user says:
|
||||
|
||||
- “用 Ant Design 组件”
|
||||
- “用 antd”
|
||||
- “用 Material UI”
|
||||
- “用 shadcn/ui”
|
||||
- “用 Bootstrap”
|
||||
- “做成 MUI 风格”
|
||||
|
||||
Important:
|
||||
|
||||
“Use Ant Design” means use Ant Design’s public component system. It does not mean copying Ant Group’s proprietary products, brand assets, internal dashboards, or non-public patterns.
|
||||
|
||||
---
|
||||
|
||||
### 4. Public Brand Inspiration
|
||||
|
||||
Use this when the user mentions a well-known brand or product style, but does not provide official assets or authorization.
|
||||
|
||||
Examples:
|
||||
|
||||
- Apple style
|
||||
- Linear style
|
||||
- Notion style
|
||||
- Stripe style
|
||||
- Airbnb style
|
||||
- Nike style
|
||||
- 小红书风格
|
||||
- magazine style
|
||||
- premium style
|
||||
|
||||
Priority:
|
||||
|
||||
Medium.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Treat as high-level inspiration only.
|
||||
- Extract abstract qualities, not proprietary expression.
|
||||
- Create an original design.
|
||||
- Do not copy logos, brand assets, distinctive layouts, exact page structures, proprietary UI patterns, or internal product patterns.
|
||||
- Do not claim the output uses an official design system.
|
||||
|
||||
Allowed to extract:
|
||||
|
||||
- mood
|
||||
- restraint
|
||||
- density
|
||||
- typography direction
|
||||
- spacing rhythm
|
||||
- content structure
|
||||
- interaction feel
|
||||
- motion temperament
|
||||
- visual temperature
|
||||
- hierarchy style
|
||||
|
||||
Not allowed:
|
||||
|
||||
- copying branded layouts
|
||||
- using logos or trademarks as design assets
|
||||
- recreating proprietary product screens
|
||||
- mimicking distinctive commercial UI too closely
|
||||
- claiming official system usage
|
||||
|
||||
Example:
|
||||
|
||||
“Apple style” means Apple-inspired qualities such as restraint, premium spacing, strong typography, product focus, and polished motion.
|
||||
|
||||
It does not mean copying apple.com, using Apple assets, or recreating Apple proprietary UI.
|
||||
|
||||
---
|
||||
|
||||
### 5. Preset HTML Template
|
||||
|
||||
Use this only when the user explicitly selects a preset HTML template.
|
||||
|
||||
Priority:
|
||||
|
||||
High when selected.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Treat the selected template as a structured starting point.
|
||||
- Preserve useful layout, responsive behavior, section structure, and interaction patterns.
|
||||
- Replace placeholder content with user-provided or clearly inferred content.
|
||||
- Remove sections that do not serve the user’s goal.
|
||||
- Do not fill unused template slots with fake content.
|
||||
- Adapt visual style, copy, density, and parameters to the user’s request.
|
||||
- Preserve editable structure for comment edits, parameter edits, and future export.
|
||||
- Avoid rewriting the whole template when a local edit is enough.
|
||||
|
||||
Do not apply a preset template by default if the user did not select one.
|
||||
|
||||
Use when the user says:
|
||||
|
||||
- “用这个模板”
|
||||
- “基于这个 HTML 模板改”
|
||||
- “我选这个模板”
|
||||
- “按这个模板生成”
|
||||
- “从这个模板开始”
|
||||
|
||||
---
|
||||
|
||||
### 6. Prior Artifact Reference
|
||||
|
||||
Use this when the user asks to continue, match, reuse, or modify a previous artifact.
|
||||
|
||||
Examples:
|
||||
|
||||
- “沿用刚才那个风格”
|
||||
- “保持上一版的感觉”
|
||||
- “这个页面也用同样的布局”
|
||||
- “把这个模板复用到另一个页面”
|
||||
- “只改文案,视觉不要动”
|
||||
|
||||
Priority:
|
||||
|
||||
Very high for follow-up work.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Treat the current or previous artifact as the active design system.
|
||||
- Preserve approved visual direction.
|
||||
- Preserve layout, rhythm, typography, components, and parameters unless the user asks to change them.
|
||||
- Make local edits when possible.
|
||||
- Do not restart from a new style unless the user explicitly requests it.
|
||||
|
||||
---
|
||||
|
||||
## Selection Rules
|
||||
|
||||
When selecting a design system reference:
|
||||
|
||||
1. Prefer explicit user choice over inferred style.
|
||||
2. Prefer user-owned systems over public systems.
|
||||
3. Prefer a selected HTML template over generic scenario patterns.
|
||||
4. Prefer style skills for general style requests.
|
||||
5. Prefer open-source component systems when implementation depends on components.
|
||||
6. Prefer brand inspiration only for mood and visual direction.
|
||||
7. Prefer the current artifact when editing or continuing existing work.
|
||||
8. Do not load multiple design systems unless the user asks to combine them.
|
||||
9. If multiple references conflict, prioritize the one closest to the user’s actual artifact or explicit request.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
“用 Ant Design 做一个后台原型”
|
||||
→ `prototype.md` + `design-system-reference.md`
|
||||
→ `design-systems/style-skills/ant/` or an open-source Ant Design reference
|
||||
|
||||
“Apple 风格的个人作品集”
|
||||
→ `portfolio.md` + `design-system-reference.md`
|
||||
→ `design-systems/brand-inspiration/apple/`
|
||||
|
||||
“editorial 风格的作品集”
|
||||
→ `portfolio.md` + `design-system-reference.md`
|
||||
→ `design-systems/style-skills/editorial/`
|
||||
|
||||
“按这个 HTML 模板做产品落地页”
|
||||
→ `landing-page.md` + `design-system-reference.md`
|
||||
→ selected preset HTML template
|
||||
|
||||
“沿用上一版风格,改成 H5 工具”
|
||||
→ `web-tool.md` + `design-system-reference.md`
|
||||
→ prior artifact reference
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Conflict Rules
|
||||
|
||||
When references conflict:
|
||||
|
||||
### User content beats template slots
|
||||
|
||||
If a template has sections the user did not ask for, remove them instead of inventing content.
|
||||
|
||||
### User-selected template beats generic scenario pattern
|
||||
|
||||
Preserve the selected template’s useful structure unless it conflicts with the user’s goal.
|
||||
|
||||
### User-owned design system beats public inspiration
|
||||
|
||||
If the user provides a brand guideline, use it over “Apple style” or other public inspiration.
|
||||
|
||||
### Style skill beats vague mood
|
||||
|
||||
If the user explicitly asks for `editorial`, use the selected style skill instead of loosely guessing what editorial means.
|
||||
|
||||
### Open-source component API beats visual imitation
|
||||
|
||||
If the user asks to use Ant Design, use Ant Design components correctly instead of merely imitating their appearance.
|
||||
|
||||
### Current artifact beats new direction during edits
|
||||
|
||||
If the user asks for a small edit, preserve the current artifact unless they explicitly request a style change.
|
||||
|
||||
---
|
||||
|
||||
## Usage Rules
|
||||
|
||||
When applying a design reference:
|
||||
|
||||
- Do not blindly copy the reference.
|
||||
- Extract what is relevant to the current scenario.
|
||||
- Preserve the user’s content and goal.
|
||||
- Keep the artifact editable.
|
||||
- Avoid unnecessary redesign.
|
||||
- Avoid mixing unrelated systems.
|
||||
- Do not introduce visual rules from a lower-priority reference when a higher-priority reference already provides guidance.
|
||||
|
||||
When adapting a reference:
|
||||
|
||||
- remove irrelevant sections
|
||||
- simplify when the reference is too complex
|
||||
- preserve useful responsive behavior
|
||||
- preserve useful interaction patterns
|
||||
- keep component naming and structure clear
|
||||
- record assumptions when useful
|
||||
|
||||
---
|
||||
|
||||
## IP and Brand Safety
|
||||
|
||||
Do not recreate copyrighted or proprietary designs.
|
||||
|
||||
Refuse or redirect if the user asks to:
|
||||
|
||||
- copy a company’s distinctive UI
|
||||
- recreate a proprietary product screen
|
||||
- use logos or brand assets without authorization
|
||||
- mimic a proprietary command structure
|
||||
- clone a branded website or app too closely
|
||||
|
||||
Instead, create an original design inspired by high-level qualities.
|
||||
|
||||
Safe phrasing:
|
||||
|
||||
```text
|
||||
I can create an original design with similar high-level qualities, such as restrained typography, premium spacing, and product-focused composition, but I should not copy the exact Apple layout or assets.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Output Expectations
|
||||
|
||||
After applying this skill, the design should show:
|
||||
|
||||
- clearer visual direction
|
||||
- better consistency
|
||||
- reduced generic styling
|
||||
- better fit to the user’s stated reference
|
||||
- appropriate use of templates, components, style skills, or inspiration
|
||||
- no unauthorized copying
|
||||
- preserved editability
|
||||
|
||||
When useful, record:
|
||||
|
||||
- reference type
|
||||
- selected design system, style skill, brand inspiration, or template
|
||||
- files consulted
|
||||
- assumptions made
|
||||
- sections kept or removed
|
||||
- parameters changed
|
||||
- license or attribution concerns
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Chinese Typography Reference
|
||||
|
||||
Use `horizontal-craft/chinese-typography.md` when the artifact contains substantial Chinese / Chinese / CJK text, Chinese editorial layout, public-account formatting, Xiaohongshu content, Chinese deck typography, Chinese UI labels, or print-design-inspired HTML.
|
||||
|
||||
Translate print-design methods into HTML structure: 版心, 网格, 留白, 标题组, 图版, 边注, 章节 rhythm, and proper punctuation.
|
||||
|
||||
|
||||
## Quality Gate
|
||||
|
||||
Before delivering this scenario, apply `quality-gate.md`.
|
||||
|
||||
This scenario may add stricter checks, but it must not weaken the shared gate.
|
||||
|
||||
## Handoff
|
||||
|
||||
After selecting and applying the reference, return to the primary artifact skill.
|
||||
|
||||
Examples:
|
||||
|
||||
- selected `apple` brand inspiration → return to `portfolio.md`
|
||||
- selected `ant` or `shadcn` style skill → return to the relevant artifact skill
|
||||
- selected HTML template → return to the relevant artifact skill
|
||||
- selected previous artifact → return to the relevant artifact skill
|
||||
|
||||
This skill guides visual and system constraints. The artifact skill still owns artifact structure and user goal.
|
||||
Executable
+281
@@ -0,0 +1,281 @@
|
||||
# Design System Reference Index
|
||||
|
||||
Use this index to select one relevant design reference.
|
||||
|
||||
Do not load all references. Choose the closest match to the user's explicit request and active artifact.
|
||||
|
||||
## Library Layout
|
||||
|
||||
```text
|
||||
SKILLS/design/design-systems/
|
||||
├── INDEX.md
|
||||
├── style-skills/
|
||||
│ └── [style-name]/
|
||||
│ ├── DESIGN.md
|
||||
│ ├── components.html # optional
|
||||
│ └── tokens.css # optional
|
||||
└── brand-inspiration/
|
||||
└── [brand-name]/
|
||||
├── DESIGN.md
|
||||
├── components.html # optional
|
||||
└── tokens.css # optional
|
||||
```
|
||||
|
||||
## Selection Rule
|
||||
|
||||
- Use `style-skills/` when the user asks for a general visual style, mood, UI style, product surface style, or reusable design recipe.
|
||||
- Use `brand-inspiration/` when the user references a public brand or product style.
|
||||
- Treat brand references as inspiration only. Do not copy logos, proprietary layouts, branded assets, or distinctive commercial UI.
|
||||
- If a reference includes `components.html` or `tokens.css`, use those as stronger implementation references when the user asks for component fidelity.
|
||||
- Do not combine multiple references unless the user explicitly asks.
|
||||
|
||||
---
|
||||
|
||||
# Style Skills
|
||||
|
||||
Path: `SKILLS/design/design-systems/style-skills/`
|
||||
|
||||
Available style skills:
|
||||
|
||||
> **按风格气质选(用户说"清新/高级/科技/复古/温暖/极简"等模糊词而非点名系统时)**:
|
||||
> 读同目录 `index.json`,按每个条目的 `mood` / `formality` / `scheme` 标签匹配用户描述,
|
||||
> 选气质最接近的 style-skill。下面列表的一句话描述只是补充;`index.json` 的标签才是匹配主依据。
|
||||
|
||||
- **agentic** — Path: `style-skills/agentic/`
|
||||
- **ant** — Path: `style-skills/ant/` — Ant Design / enterprise component-oriented UI; use when user explicitly asks for Ant Design or antd-like enterprise UI.
|
||||
- **application** — Path: `style-skills/application/` — Product/application UI patterns.
|
||||
- **artistic** — Path: `style-skills/artistic/`
|
||||
- **atelier-zero** — Path: `style-skills/atelier-zero/` — Minimal studio / atelier-style visual system.
|
||||
- **bento** — Path: `style-skills/bento/`
|
||||
- **bold** — Path: `style-skills/bold/`
|
||||
- **brutalism** — Path: `style-skills/brutalism/` — Raw structure, direct hierarchy, intentionally rough visual energy.
|
||||
- **cafe** — Path: `style-skills/cafe/`
|
||||
- **claude** — Path: `style-skills/claude/`
|
||||
- **claymorphism** — Path: `style-skills/claymorphism/`
|
||||
- **clean** — Path: `style-skills/clean/` — Clean, neutral, orderly composition.
|
||||
- **codex** — Path: `style-skills/codex/`
|
||||
- **colorful** — Path: `style-skills/colorful/`
|
||||
- **contemporary** — Path: `style-skills/contemporary/`
|
||||
- **corporate** — Path: `style-skills/corporate/` — Business-facing, formal, structured visual language.
|
||||
- **cosmic** — Path: `style-skills/cosmic/`
|
||||
- **creative** — Path: `style-skills/creative/`
|
||||
- **dashboard** — Path: `style-skills/dashboard/` — Dense data/application interface patterns.
|
||||
- **default** — Path: `style-skills/default/` — Neutral general-purpose design system fallback. — includes components, tokens
|
||||
- **dithered** — Path: `style-skills/dithered/`
|
||||
- **doodle** — Path: `style-skills/doodle/`
|
||||
- **dramatic** — Path: `style-skills/dramatic/`
|
||||
- **editorial** — Path: `style-skills/editorial/` — Magazine-like typography, strong content rhythm, art-directed layout.
|
||||
- **elegant** — Path: `style-skills/elegant/`
|
||||
- **energetic** — Path: `style-skills/energetic/`
|
||||
- **enterprise** — Path: `style-skills/enterprise/` — B2B/enterprise clarity, dense but controlled components.
|
||||
- **expressive** — Path: `style-skills/expressive/`
|
||||
- **fantasy** — Path: `style-skills/fantasy/`
|
||||
- **fiction** — Path: `style-skills/fiction/`
|
||||
- **flat** — Path: `style-skills/flat/`
|
||||
- **friendly** — Path: `style-skills/friendly/`
|
||||
- **futuristic** — Path: `style-skills/futuristic/`
|
||||
- **glassmorphism** — Path: `style-skills/glassmorphism/` — Translucent layered surfaces; use sparingly.
|
||||
- **gradient** — Path: `style-skills/gradient/`
|
||||
- **hud** — Path: `style-skills/hud/` — Heads-up-display / operational interface aesthetic.
|
||||
- **immersive** — Path: `style-skills/immersive/`
|
||||
- **impeccable** — Path: `style-skills/impeccable/`
|
||||
- **levels** — Path: `style-skills/levels/`
|
||||
- **lingo** — Path: `style-skills/lingo/`
|
||||
- **luxury** — Path: `style-skills/luxury/` — Premium spacing, refined contrast, elegant restraint.
|
||||
- **material** — Path: `style-skills/material/` — Material Design-like component system and interaction clarity.
|
||||
- **matrix** — Path: `style-skills/matrix/`
|
||||
- **minimal** — Path: `style-skills/minimal/` — Restrained, low-decoration, content-first layout.
|
||||
- **mission-control** — Path: `style-skills/mission-control/` — Command center and control-room inspired product surfaces.
|
||||
- **modern** — Path: `style-skills/modern/`
|
||||
- **mono** — Path: `style-skills/mono/`
|
||||
- **neobrutalism** — Path: `style-skills/neobrutalism/` — Bold borders, high contrast, raw visual energy.
|
||||
- **neon** — Path: `style-skills/neon/`
|
||||
- **neumorphism** — Path: `style-skills/neumorphism/`
|
||||
- **pacman** — Path: `style-skills/pacman/`
|
||||
- **paper** — Path: `style-skills/paper/` — Tactile paper-like surfaces and editorial warmth.
|
||||
- **parchment** — Path: `style-skills/parchment/` — Warm parchment, ink-blue accent, serif-led print/editorial system for formal & long-form documents (résumé, report, white paper, letter). Pairs with content-page; not a product-UI style.
|
||||
- **perspective** — Path: `style-skills/perspective/`
|
||||
- **premium** — Path: `style-skills/premium/`
|
||||
- **professional** — Path: `style-skills/professional/` — Conservative professional presentation, clarity, trust, low-risk polish.
|
||||
- **publication** — Path: `style-skills/publication/`
|
||||
- **refined** — Path: `style-skills/refined/`
|
||||
- **retro** — Path: `style-skills/retro/` — Nostalgic visual language.
|
||||
- **riso** — Path: `style-skills/riso/`
|
||||
- **sega** — Path: `style-skills/sega/`
|
||||
- **shadcn** — Path: `style-skills/shadcn/` — Modern Tailwind/shadcn-style component composition; use when user asks for shadcn/ui or modern composable UI.
|
||||
- **simple** — Path: `style-skills/simple/`
|
||||
- **sketch** — Path: `style-skills/sketch/`
|
||||
- **skeumorphism** — Path: `style-skills/skeumorphism/`
|
||||
- **sleek** — Path: `style-skills/sleek/`
|
||||
- **spacious** — Path: `style-skills/spacious/`
|
||||
- **storytelling** — Path: `style-skills/storytelling/`
|
||||
- **terracotta** — Path: `style-skills/terracotta/`
|
||||
- **tetris** — Path: `style-skills/tetris/`
|
||||
- **totality-festival** — Path: `style-skills/totality-festival/`
|
||||
- **trading-terminal** — Path: `style-skills/trading-terminal/` — Dense financial/trading terminal aesthetic.
|
||||
- **urdu** — Path: `style-skills/urdu/`
|
||||
- **vibrant** — Path: `style-skills/vibrant/`
|
||||
- **vintage** — Path: `style-skills/vintage/`
|
||||
- **warm-editorial** — Path: `style-skills/warm-editorial/` — Warm editorial system with human texture.
|
||||
|
||||
---
|
||||
|
||||
# Brand Inspiration
|
||||
|
||||
Path: `SKILLS/design/design-systems/brand-inspiration/`
|
||||
|
||||
Use only as high-level inspiration unless the user provides owned assets or an official/open design system.
|
||||
|
||||
Available brand inspiration references:
|
||||
|
||||
- **airbnb** — Path: `brand-inspiration/airbnb/` — Warm human-centered marketplace feel. — includes components, tokens
|
||||
- **airtable** — Path: `brand-inspiration/airtable/`
|
||||
- **apple** — Path: `brand-inspiration/apple/` — Restraint, premium spacing, clear typography, product focus. — includes components, tokens
|
||||
- **arc** — Path: `brand-inspiration/arc/`
|
||||
- **binance** — Path: `brand-inspiration/binance/`
|
||||
- **bmw** — Path: `brand-inspiration/bmw/`
|
||||
- **bmw-m** — Path: `brand-inspiration/bmw-m/`
|
||||
- **bugatti** — Path: `brand-inspiration/bugatti/`
|
||||
- **cal** — Path: `brand-inspiration/cal/`
|
||||
- **canva** — Path: `brand-inspiration/canva/`
|
||||
- **cisco** — Path: `brand-inspiration/cisco/`
|
||||
- **claude** — Path: `brand-inspiration/claude/`
|
||||
- **clay** — Path: `brand-inspiration/clay/`
|
||||
- **clickhouse** — Path: `brand-inspiration/clickhouse/`
|
||||
- **cohere** — Path: `brand-inspiration/cohere/`
|
||||
- **coinbase** — Path: `brand-inspiration/coinbase/`
|
||||
- **composio** — Path: `brand-inspiration/composio/`
|
||||
- **cursor** — Path: `brand-inspiration/cursor/` — includes components, tokens
|
||||
- **discord** — Path: `brand-inspiration/discord/` — Community product UI with playful but structured interface patterns. — includes components, tokens
|
||||
- **duolingo** — Path: `brand-inspiration/duolingo/`
|
||||
- **elevenlabs** — Path: `brand-inspiration/elevenlabs/`
|
||||
- **expo** — Path: `brand-inspiration/expo/`
|
||||
- **ferrari** — Path: `brand-inspiration/ferrari/`
|
||||
- **figma** — Path: `brand-inspiration/figma/` — Collaborative product energy, structured color, approachable creation. — includes components, tokens
|
||||
- **framer** — Path: `brand-inspiration/framer/`
|
||||
- **github** — Path: `brand-inspiration/github/` — Developer collaboration, code/product clarity, utility-first UI. — includes components, tokens
|
||||
- **hashicorp** — Path: `brand-inspiration/hashicorp/`
|
||||
- **huggingface** — Path: `brand-inspiration/huggingface/`
|
||||
- **ibm** — Path: `brand-inspiration/ibm/`
|
||||
- **intercom** — Path: `brand-inspiration/intercom/`
|
||||
- **kami** — Path: `brand-inspiration/kami/` — includes components, tokens
|
||||
- **kraken** — Path: `brand-inspiration/kraken/`
|
||||
- **lamborghini** — Path: `brand-inspiration/lamborghini/`
|
||||
- **linear-app** — Path: `brand-inspiration/linear-app/` — Precise product UI, dark restraint, calm density. — includes components, tokens
|
||||
- **loom** — Path: `brand-inspiration/loom/`
|
||||
- **lovable** — Path: `brand-inspiration/lovable/`
|
||||
- **mastercard** — Path: `brand-inspiration/mastercard/`
|
||||
- **meta** — Path: `brand-inspiration/meta/`
|
||||
- **minimax** — Path: `brand-inspiration/minimax/`
|
||||
- **mintlify** — Path: `brand-inspiration/mintlify/`
|
||||
- **miro** — Path: `brand-inspiration/miro/`
|
||||
- **mistral-ai** — Path: `brand-inspiration/mistral-ai/`
|
||||
- **mistral.ai** — Path: `brand-inspiration/mistral.ai/`
|
||||
- **mongodb** — Path: `brand-inspiration/mongodb/`
|
||||
- **nike** — Path: `brand-inspiration/nike/` — Bold athletic energy, campaign-driven impact, high contrast.
|
||||
- **notion** — Path: `brand-inspiration/notion/` — Calm productivity, document-like structure, low visual noise. — includes components, tokens
|
||||
- **nvidia** — Path: `brand-inspiration/nvidia/`
|
||||
- **ollama** — Path: `brand-inspiration/ollama/`
|
||||
- **openai** — Path: `brand-inspiration/openai/` — AI product clarity, calm technical presentation, restrained futurism. — includes components, tokens
|
||||
- **opencode-ai** — Path: `brand-inspiration/opencode-ai/`
|
||||
- **opencode.ai** — Path: `brand-inspiration/opencode.ai/`
|
||||
- **perplexity** — Path: `brand-inspiration/perplexity/`
|
||||
- **pinterest** — Path: `brand-inspiration/pinterest/`
|
||||
- **playstation** — Path: `brand-inspiration/playstation/`
|
||||
- **posthog** — Path: `brand-inspiration/posthog/`
|
||||
- **raycast** — Path: `brand-inspiration/raycast/`
|
||||
- **renault** — Path: `brand-inspiration/renault/`
|
||||
- **replicate** — Path: `brand-inspiration/replicate/`
|
||||
- **resend** — Path: `brand-inspiration/resend/`
|
||||
- **revolut** — Path: `brand-inspiration/revolut/`
|
||||
- **runwayml** — Path: `brand-inspiration/runwayml/`
|
||||
- **sanity** — Path: `brand-inspiration/sanity/`
|
||||
- **sentry** — Path: `brand-inspiration/sentry/`
|
||||
- **shopify** — Path: `brand-inspiration/shopify/` — Commerce enablement, friendly business clarity. — includes components, tokens
|
||||
- **slack** — Path: `brand-inspiration/slack/` — includes components, tokens
|
||||
- **spacex** — Path: `brand-inspiration/spacex/`
|
||||
- **spotify** — Path: `brand-inspiration/spotify/` — Bold media energy, strong color and motion potential. — includes components, tokens
|
||||
- **starbucks** — Path: `brand-inspiration/starbucks/`
|
||||
- **stripe** — Path: `brand-inspiration/stripe/` — Polished SaaS clarity, developer/business trust, careful gradients. — includes components, tokens
|
||||
- **supabase** — Path: `brand-inspiration/supabase/`
|
||||
- **superhuman** — Path: `brand-inspiration/superhuman/`
|
||||
- **tesla** — Path: `brand-inspiration/tesla/` — Futuristic product focus, minimal copy, strong hero emphasis.
|
||||
- **theverge** — Path: `brand-inspiration/theverge/`
|
||||
- **together-ai** — Path: `brand-inspiration/together-ai/`
|
||||
- **together.ai** — Path: `brand-inspiration/together.ai/`
|
||||
- **uber** — Path: `brand-inspiration/uber/` — includes components, tokens
|
||||
- **vercel** — Path: `brand-inspiration/vercel/` — Developer-focused minimalism, sharp contrast, deployment/product clarity. — includes components, tokens
|
||||
- **vodafone** — Path: `brand-inspiration/vodafone/`
|
||||
- **voltagent** — Path: `brand-inspiration/voltagent/`
|
||||
- **warp** — Path: `brand-inspiration/warp/`
|
||||
- **webex** — Path: `brand-inspiration/webex/`
|
||||
- **webflow** — Path: `brand-inspiration/webflow/`
|
||||
- **wechat** — Path: `brand-inspiration/wechat/` — Social/messaging ecosystem inspiration; use cautiously as high-level reference.
|
||||
- **wired** — Path: `brand-inspiration/wired/`
|
||||
- **wise** — Path: `brand-inspiration/wise/`
|
||||
- **x-ai** — Path: `brand-inspiration/x-ai/`
|
||||
- **x.ai** — Path: `brand-inspiration/x.ai/`
|
||||
- **xiaohongshu** — Path: `brand-inspiration/xiaohongshu/` — Content/community inspiration; use as high-level style and content rhythm only.
|
||||
- **zapier** — Path: `brand-inspiration/zapier/`
|
||||
|
||||
---
|
||||
|
||||
## Recently Added from open-design
|
||||
|
||||
The following references were added or enriched from `nexu-io/open-design`:
|
||||
|
||||
|
||||
### New references
|
||||
|
||||
- `brand-inspiration/arc/`
|
||||
- `style-skills/atelier-zero/`
|
||||
- `brand-inspiration/canva/`
|
||||
- `brand-inspiration/cisco/`
|
||||
- `style-skills/default/`
|
||||
- `brand-inspiration/discord/`
|
||||
- `brand-inspiration/duolingo/`
|
||||
- `brand-inspiration/github/`
|
||||
- `style-skills/hud/`
|
||||
- `brand-inspiration/huggingface/`
|
||||
- `brand-inspiration/kami/`
|
||||
- `brand-inspiration/linear-app/`
|
||||
- `brand-inspiration/loom/`
|
||||
- `style-skills/mission-control/`
|
||||
- `brand-inspiration/mistral-ai/`
|
||||
- `brand-inspiration/openai/`
|
||||
- `brand-inspiration/opencode-ai/`
|
||||
- `brand-inspiration/perplexity/`
|
||||
- `brand-inspiration/together-ai/`
|
||||
- `style-skills/totality-festival/`
|
||||
- `style-skills/trading-terminal/`
|
||||
- `style-skills/urdu/`
|
||||
- `style-skills/warm-editorial/`
|
||||
- `brand-inspiration/webex/`
|
||||
- `brand-inspiration/wechat/`
|
||||
- `brand-inspiration/x-ai/`
|
||||
- `brand-inspiration/xiaohongshu/`
|
||||
|
||||
### Enriched with components/tokens
|
||||
|
||||
- `brand-inspiration/figma/components.html`
|
||||
- `brand-inspiration/figma/tokens.css`
|
||||
- `brand-inspiration/apple/components.html`
|
||||
- `brand-inspiration/apple/tokens.css`
|
||||
- `brand-inspiration/notion/components.html`
|
||||
- `brand-inspiration/notion/tokens.css`
|
||||
- `brand-inspiration/uber/components.html`
|
||||
- `brand-inspiration/uber/tokens.css`
|
||||
- `brand-inspiration/stripe/components.html`
|
||||
- `brand-inspiration/stripe/tokens.css`
|
||||
- `brand-inspiration/vercel/components.html`
|
||||
- `brand-inspiration/vercel/tokens.css`
|
||||
- `brand-inspiration/shopify/components.html`
|
||||
- `brand-inspiration/shopify/tokens.css`
|
||||
- `brand-inspiration/spotify/components.html`
|
||||
- `brand-inspiration/spotify/tokens.css`
|
||||
- `brand-inspiration/airbnb/components.html`
|
||||
- `brand-inspiration/airbnb/tokens.css`
|
||||
- `brand-inspiration/slack/components.html`
|
||||
- `brand-inspiration/slack/tokens.css`
|
||||
- `brand-inspiration/cursor/components.html`
|
||||
- `brand-inspiration/cursor/tokens.css`
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Brand 标签受控词表
|
||||
|
||||
`index.json` 的 `brand_inspiration` 条目使用下列受控词表打多维标签。**新增或修改品牌标签时,
|
||||
词条应从此表中选**,避免近义词分裂(如"克制 / 简约 / 极简")导致检索漏匹配。
|
||||
|
||||
词表从 89 个品牌 DESIGN.md 的真实内容归纳而来,并与 `deck-templates` 的 `template.json`
|
||||
维度设计对齐。
|
||||
|
||||
---
|
||||
|
||||
## `industry`(行业,数组,≥1)
|
||||
AI · 开发者工具 · 生产力/协作 · 金融/支付 · 汽车 · 奢侈品 · 社交/通讯 · 电商/零售 ·
|
||||
媒体/出版 · 基础设施/企业 · 消费电子/科技 · 内容/创意工具 · 出行/本地生活 · 电信 · 文档/教育
|
||||
|
||||
## `product_type`(产品形态,数组,≥1)
|
||||
SaaS · 官网/营销页 · 开发者平台 · 消费App · 内容平台 · 电商/交易 · 品牌展示页 ·
|
||||
产品发布页 · 文档站 · 作品集/个人
|
||||
|
||||
## `mood`(情绪气质,数组,≥2)
|
||||
极简 · 克制 · 精密/工程感 · 技术感 · 奢华 · 戏剧化 · 暗黑原生 · 影像优先 · 编辑/杂志感 ·
|
||||
温暖 · 亲和/友好 · 活力/明亮 · 内容优先 · 企业/可信 · 渐变氛围 · 插画丰富 · 复古/玩味 · 未来/科幻
|
||||
|
||||
说明(保留的近义区分,刻意不合并):
|
||||
- `极简` = 结构性减法;`克制` = 安静不喧哗。二者可共存也可单独出现。
|
||||
- `精密/工程感` = 精确校准的视觉;`技术感` = 代码/开发者气质。
|
||||
|
||||
## 单值维度
|
||||
- `formality`(正式度):high / medium / low
|
||||
- `density`(信息密度):high / medium / low
|
||||
- `scheme`(明暗):light / dark / both
|
||||
- `era`(年代感):复古 / 现代 / 未来
|
||||
|
||||
## 自然语言字段
|
||||
- `best_for`:最适合的场景(一句话)
|
||||
- `avoid_for`:不适合的场景(一句话)
|
||||
|
||||
---
|
||||
|
||||
## 检索用法
|
||||
按需求维度过滤 `index.json` → 锁定候选品牌 → 只读命中品牌的 `DESIGN.md` 获取完整 token 与规则。
|
||||
数组维度支持多对多:一个品牌可在同一维度命中多个值(如 Stripe 同属 金融/支付 与 开发者工具)。
|
||||
@@ -0,0 +1,545 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Airbnb
|
||||
description: "A warm, generous consumer marketplace anchored on a clean white canvas and Airbnb Rausch (#ff385c), the single brand voltage that carries every primary CTA, search-button orb, and rating dot. Type runs Airbnb Cereal VF at modest weights — display sits at 22–28px in weight 500/600 rather than the heavy 700+ that fintech and enterprise systems use; the brand trusts photography and generous whitespace over typographic muscle. Three product entries (Homes, Experiences, Services) sit in the top nav with hand-illustrated 32-icon glyphs and \"NEW\" badges, signaling a marketplace expansion rather than a feature dump. Pill-shaped search bars (`{rounded.full}`), softly rounded property cards (`{rounded.lg}` ~14px), and 32px button radii read as friendly and human — there is no hard corner anywhere except the body grid."
|
||||
|
||||
colors:
|
||||
primary: "#ff385c"
|
||||
primary-active: "#e00b41"
|
||||
primary-disabled: "#ffd1da"
|
||||
primary-error-text: "#c13515"
|
||||
primary-error-text-hover: "#b32505"
|
||||
luxe: "#460479"
|
||||
plus: "#92174d"
|
||||
ink: "#222222"
|
||||
body: "#3f3f3f"
|
||||
muted: "#6a6a6a"
|
||||
muted-soft: "#929292"
|
||||
hairline: "#dddddd"
|
||||
hairline-soft: "#ebebeb"
|
||||
border-strong: "#c1c1c1"
|
||||
canvas: "#ffffff"
|
||||
surface-soft: "#f7f7f7"
|
||||
surface-card: "#ffffff"
|
||||
surface-strong: "#f2f2f2"
|
||||
on-primary: "#ffffff"
|
||||
on-dark: "#ffffff"
|
||||
legal-link: "#428bff"
|
||||
star-rating: "#222222"
|
||||
scrim: "#000000"
|
||||
|
||||
typography:
|
||||
display-xl:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, -apple-system, system-ui, Roboto, 'Helvetica Neue', sans-serif"
|
||||
fontSize: 28px
|
||||
fontWeight: 700
|
||||
lineHeight: 1.43
|
||||
letterSpacing: 0
|
||||
display-lg:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 22px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.18
|
||||
letterSpacing: -0.44px
|
||||
display-md:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 21px
|
||||
fontWeight: 700
|
||||
lineHeight: 1.43
|
||||
letterSpacing: 0
|
||||
display-sm:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 20px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.20
|
||||
letterSpacing: -0.18px
|
||||
title-md:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 16px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.25
|
||||
letterSpacing: 0
|
||||
title-sm:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 16px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.25
|
||||
letterSpacing: 0
|
||||
rating-display:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 64px
|
||||
fontWeight: 700
|
||||
lineHeight: 1.1
|
||||
letterSpacing: -1px
|
||||
body-md:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 16px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.5
|
||||
letterSpacing: 0
|
||||
body-sm:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.43
|
||||
letterSpacing: 0
|
||||
caption:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.29
|
||||
letterSpacing: 0
|
||||
caption-sm:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 13px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.23
|
||||
letterSpacing: 0
|
||||
badge:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 11px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.18
|
||||
letterSpacing: 0
|
||||
micro-label:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 12px
|
||||
fontWeight: 700
|
||||
lineHeight: 1.33
|
||||
letterSpacing: 0
|
||||
uppercase-tag:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 8px
|
||||
fontWeight: 700
|
||||
lineHeight: 1.25
|
||||
letterSpacing: 0.32px
|
||||
textTransform: uppercase
|
||||
button-md:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 16px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.25
|
||||
letterSpacing: 0
|
||||
button-sm:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.29
|
||||
letterSpacing: 0
|
||||
link:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.43
|
||||
letterSpacing: 0
|
||||
nav-link:
|
||||
fontFamily: "'Airbnb Cereal VF', Circular, sans-serif"
|
||||
fontSize: 16px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.25
|
||||
letterSpacing: 0
|
||||
|
||||
rounded:
|
||||
none: 0px
|
||||
xs: 4px
|
||||
sm: 8px
|
||||
md: 14px
|
||||
lg: 20px
|
||||
xl: 32px
|
||||
full: 9999px
|
||||
|
||||
spacing:
|
||||
xxs: 2px
|
||||
xs: 4px
|
||||
sm: 8px
|
||||
md: 12px
|
||||
base: 16px
|
||||
lg: 24px
|
||||
xl: 32px
|
||||
xxl: 48px
|
||||
section: 64px
|
||||
|
||||
components:
|
||||
button-primary:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.button-md}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: 14px 24px
|
||||
height: 48px
|
||||
button-primary-active:
|
||||
backgroundColor: "{colors.primary-active}"
|
||||
textColor: "{colors.on-primary}"
|
||||
rounded: "{rounded.sm}"
|
||||
button-primary-disabled:
|
||||
backgroundColor: "{colors.primary-disabled}"
|
||||
textColor: "{colors.on-primary}"
|
||||
rounded: "{rounded.sm}"
|
||||
button-secondary:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.button-md}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: 13px 23px
|
||||
height: 48px
|
||||
button-tertiary-text:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.button-md}"
|
||||
button-pill-rausch:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.button-sm}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: 10px 20px
|
||||
search-orb:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
rounded: "{rounded.full}"
|
||||
height: 48px
|
||||
icon-button-circle:
|
||||
backgroundColor: "{colors.surface-strong}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.full}"
|
||||
height: 32px
|
||||
icon-button-outline:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.full}"
|
||||
height: 40px
|
||||
top-nav:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.nav-link}"
|
||||
height: 80px
|
||||
product-tab-active:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.nav-link}"
|
||||
rounded: "{rounded.none}"
|
||||
product-tab-inactive:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.muted}"
|
||||
typography: "{typography.nav-link}"
|
||||
search-bar-pill:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: 14px 24px
|
||||
height: 64px
|
||||
search-field-segment:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.caption}"
|
||||
padding: 8px 24px
|
||||
category-strip:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.muted}"
|
||||
typography: "{typography.button-sm}"
|
||||
category-tab-active:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.button-sm}"
|
||||
rounded: "{rounded.none}"
|
||||
property-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.md}"
|
||||
property-card-photo:
|
||||
rounded: "{rounded.md}"
|
||||
experience-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.title-md}"
|
||||
rounded: "{rounded.md}"
|
||||
city-link-block:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.title-sm}"
|
||||
rating-display-card:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.rating-display}"
|
||||
guest-favorite-badge:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.badge}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: 4px 10px
|
||||
new-tag:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.uppercase-tag}"
|
||||
rounded: "{rounded.full}"
|
||||
padding: 2px 6px
|
||||
amenity-row:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
padding: 12px 0
|
||||
reviews-card:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
host-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: 24px
|
||||
reservation-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: 24px
|
||||
date-picker-day:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
rounded: "{rounded.full}"
|
||||
date-picker-day-selected:
|
||||
backgroundColor: "{colors.ink}"
|
||||
textColor: "{colors.on-dark}"
|
||||
rounded: "{rounded.full}"
|
||||
text-input:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: 14px 12px
|
||||
height: 56px
|
||||
footer-light:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
padding: 48px 80px
|
||||
footer-link:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-sm}"
|
||||
legal-band:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.muted}"
|
||||
typography: "{typography.caption-sm}"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Airbnb is the canonical example of a generous, photography-led consumer marketplace. The base canvas is **pure white** (`{colors.canvas}` — #ffffff) with deep near-black ink (`{colors.ink}` — #222222) for headlines and body, and a single voltage of **Rausch** (`{colors.primary}` — #ff385c) carrying every primary CTA, the search-button orb, the heart save state, and inline brand links. There is no secondary brand color in mainline marketing — the **Luxe purple** (`{colors.luxe}` — #460479) and **Plus magenta** (`{colors.plus}` — #92174d) tokens are sub-brand accents that only appear inside Airbnb Luxe / Plus contexts.
|
||||
|
||||
Type runs **Airbnb Cereal VF** (a custom variable font Airbnb licenses), with **Circular** as the historic in-house fallback and a system stack underneath. Cereal sits at modest weights — display headlines render at 22–28px in weight 500–600, not the heavy 700+ weights that financial or enterprise systems lean on. The hero h1 ("Inspiration for future getaways") on the homepage is just 28px / 700, which would feel small on a typical SaaS page; here it works because the layout leans on photography (city collage, property cards) for visual weight rather than typographic muscle.
|
||||
|
||||
The shape language is **soft**. Buttons are 8px radius (`{rounded.sm}`), property cards are ~14px (`{rounded.md}`), the search bar is fully pill-shaped (`{rounded.full}`), wishlist hearts and search orbs are circles (`{rounded.full}`), and category strip rounded corners run at 32px (`{rounded.xl}`). There is essentially no hard corner anywhere except the body grid itself — every interactive element is rounded.
|
||||
|
||||
**Key Characteristics:**
|
||||
- Single accent color: `{colors.primary}` (#ff385c — "Rausch") carries every primary CTA, the search orb, the heart save state, and the brand wordmark. Used scarcely — most pages are 90% white + ink with one or two Rausch moments.
|
||||
- Custom variable type: `Airbnb Cereal VF`. Display weights sit at 500–700, body at 400. Modest weight is intentional — the system trusts photography for visual heft.
|
||||
- Three-product top nav: Homes, Experiences, Services — each with a hand-illustrated 32px icon and "NEW" badges (`{component.new-tag}`) on the two newer products. Active tab uses an underline rule (`{component.product-tab-active}`).
|
||||
- Pill-shaped global search bar: white surface, fully rounded (`{rounded.full}`), divided by 1px hairlines into Where / When / Who segments, terminated by a circular Rausch search orb (`{component.search-orb}`).
|
||||
- Property cards are photo-first: aspect-ratio rectangles with `{rounded.md}` corner clipping, swipeable image carousel, "Guest favorite" floating badge top-left, heart icon top-right, then 4–5 lines of meta beneath.
|
||||
- Editorial dropdowns (footer, language picker) are clean text columns over the white canvas — no card surface, no shadow.
|
||||
- The design system caps elevation at one shadow tier (`box-shadow: rgba(0,0,0,0.02) 0 0 0 1px, rgba(0,0,0,0.04) 0 2px 6px, rgba(0,0,0,0.1) 0 4px 8px`) — used on hover-floated cards and search/account dropdowns.
|
||||
- 8px base spacing system, with major sections at `{spacing.section}` (64px) — generous but not airy enough to feel editorial-magazine; the marketplace density wants more cards per scroll.
|
||||
|
||||
## Colors
|
||||
|
||||
### Brand & Accent
|
||||
- **Rausch** (`{colors.primary}` — #ff385c): The single brand color. Used for primary CTA backgrounds (Reserve, Continue), the search orb, the heart save state on property cards, and inline brand links. The most recognizable color in consumer travel.
|
||||
- **Rausch Active** (`{colors.primary-active}` — #e00b41): The press / pointer-down variant — slightly more saturated. Used on `{component.button-primary-active}`.
|
||||
- **Rausch Disabled** (`{colors.primary-disabled}` — #ffd1da): A pale tint used on disabled CTAs.
|
||||
- **Luxe Purple** (`{colors.luxe}` — #460479): Sub-brand accent for Airbnb Luxe. Only appears inside Luxe-branded surfaces — never in mainline marketing.
|
||||
- **Plus Magenta** (`{colors.plus}` — #92174d): Sub-brand accent for Airbnb Plus. Same scoping as Luxe — sub-product only.
|
||||
|
||||
### Surface
|
||||
- **Canvas** (`{colors.canvas}` — #ffffff): The default page floor for every public page. Airbnb does not have a dark mode on the public web.
|
||||
- **Surface Soft** (`{colors.surface-soft}` — #f7f7f7): The lightest fill — used on disabled fields, sub-nav hover backgrounds, and the inline search filter band.
|
||||
- **Surface Strong** (`{colors.surface-strong}` — #f2f2f2): Slightly heavier fill — circular icon-button surface (e.g., the breadcrumb back-arrow and listing toolbar buttons).
|
||||
|
||||
### Hairlines & Borders
|
||||
- **Hairline** (`{colors.hairline}` — #dddddd): The default 1px border tone — search bar dividers, table separators, footer column splitters, card 1px borders.
|
||||
- **Hairline Soft** (`{colors.hairline-soft}` — #ebebeb): A lighter divider used on long-scrolling editorial body separators.
|
||||
- **Border Strong** (`{colors.border-strong}` — #c1c1c1): A heavier stroke used on disabled outline buttons and form input outlines after focus.
|
||||
|
||||
### Text
|
||||
- **Ink** (`{colors.ink}` — #222222): The dominant text color on light surfaces. Display headlines, body paragraphs, primary nav links, and most inline link text. Never pure black.
|
||||
- **Body** (`{colors.body}` — #3f3f3f): A secondary running-text color used inside long-form review and amenity copy where ink would feel too heavy.
|
||||
- **Muted** (`{colors.muted}` — #6a6a6a): Sub-titles inside city link blocks ("Cottage rentals", "Villa rentals"), inactive product-tab labels, footer category sub-labels, "View all" links.
|
||||
- **Muted Soft** (`{colors.muted-soft}` — #929292): Disabled link text. Used very sparingly.
|
||||
- **Star Rating** (`{colors.star-rating}` — #222222): The same ink token — Airbnb's star icon and "4.81" rating numbers all render in ink rather than a yellow/gold color, which is a deliberate brand choice (yellow stars feel cheap in travel context).
|
||||
- **On Primary** (`{colors.on-primary}` — #ffffff): White text on Rausch CTAs.
|
||||
|
||||
### Semantic
|
||||
- **Error** (`{colors.primary-error-text}` — #c13515): Inline error text for form validation. Distinct from Rausch — slightly darker, more saturated red.
|
||||
- **Error Hover** (`{colors.primary-error-text-hover}` — #b32505): Darkens on link hover.
|
||||
- **Legal Link Blue** (`{colors.legal-link}` — #428bff): Inline links inside legal copy (Privacy, Terms). Only used inside the legal sub-band.
|
||||
|
||||
### Scrim
|
||||
- **Scrim** (`{colors.scrim}` — #000000 at 50% opacity): The global modal backdrop tone — date picker, login dialog, language picker. Stored as the base hex; opacity is applied at render time.
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Family
|
||||
The system runs **Airbnb Cereal VF** for everything — display, body, navigation, captions, microcopy. Fallbacks walk `Circular, -apple-system, system-ui, Roboto, "Helvetica Neue", sans-serif`. **Circular** is the historic in-house typeface still kept as the first non-variable fallback; system stacks back it up.
|
||||
|
||||
There is no separate display family. The variable font carries the entire scale.
|
||||
|
||||
### Hierarchy
|
||||
|
||||
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|
||||
|---|---|---|---|---|---|
|
||||
| `{typography.rating-display}` | 64px | 700 | 1.1 | -1px | Listing detail rating display ("4.81") |
|
||||
| `{typography.display-xl}` | 28px | 700 | 1.43 | 0 | Homepage h1 ("Inspiration for future getaways") |
|
||||
| `{typography.display-lg}` | 22px | 500 | 1.18 | -0.44px | Listing detail h1 ("Close to Fethiye Aliyah Bali Beach…") |
|
||||
| `{typography.display-md}` | 21px | 700 | 1.43 | 0 | Section heads inside listing detail ("What this place offers") |
|
||||
| `{typography.display-sm}` | 20px | 600 | 1.20 | -0.18px | Sub-section titles ("Things to know") |
|
||||
| `{typography.title-md}` | 16px | 600 | 1.25 | 0 | City link block titles ("Wilmington", "Athens") |
|
||||
| `{typography.title-sm}` | 16px | 500 | 1.25 | 0 | Footer column heads ("Support", "Hosting", "Airbnb") |
|
||||
| `{typography.body-md}` | 16px | 400 | 1.5 | 0 | Default running-text inside listing copy |
|
||||
| `{typography.body-sm}` | 14px | 400 | 1.43 | 0 | Card meta lines, dates, prices, distance text |
|
||||
| `{typography.caption}` | 14px | 500 | 1.29 | 0 | Search field segment labels ("Where", "When", "Who") |
|
||||
| `{typography.caption-sm}` | 13px | 400 | 1.23 | 0 | Footer legal line ("© 2026 Airbnb, Inc.") |
|
||||
| `{typography.badge}` | 11px | 600 | 1.18 | 0 | "Guest favorite" floating badge text |
|
||||
| `{typography.micro-label}` | 12px | 700 | 1.33 | 0 | Card amenity micro-labels ("Inline 6") |
|
||||
| `{typography.uppercase-tag}` | 8px | 700 | 1.25 | 0.32px (uppercase) | "NEW" badge on product nav tabs |
|
||||
| `{typography.button-md}` | 16px | 500 | 1.25 | 0 | Primary CTA button labels |
|
||||
| `{typography.button-sm}` | 14px | 500 | 1.29 | 0 | Pill button labels (category strip) |
|
||||
| `{typography.link}` | 14px | 400 | 1.43 | 0 | Inline body links |
|
||||
| `{typography.nav-link}` | 16px | 600 | 1.25 | 0 | Top product-nav labels (Homes, Experiences, Services) |
|
||||
|
||||
### Principles
|
||||
Display weights stay modest. The homepage h1 at 28px / 700 is deliberately small — it tucks under the search bar so photography and the city-link grid carry visual hierarchy. The listing-detail h1 at 22px / 500 is even quieter; the listing photo banner does the work above it.
|
||||
|
||||
The single typographically loud moment in the entire system is the **rating display** (`{typography.rating-display}` — 64px / 700) on listing pages. That is the only place the system trusts type alone to carry hierarchy — rating numbers are a peak trust signal, so they get the loudest treatment.
|
||||
|
||||
### Note on Font Substitutes
|
||||
If Airbnb Cereal VF and Circular are unavailable, **Inter** is the closest open-source substitute. Adjust display headlines down by ~2% in line-height to match Cereal's slightly tighter cap height; otherwise the proportions transfer cleanly.
|
||||
|
||||
## Layout
|
||||
|
||||
### Spacing System
|
||||
- **Base unit:** 4px (with 2px micro-step).
|
||||
- **Tokens:** `{spacing.xxs}` 2px · `{spacing.xs}` 4px · `{spacing.sm}` 8px · `{spacing.md}` 12px · `{spacing.base}` 16px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 64px.
|
||||
- **Section padding (vertical):** `{spacing.section}` (64px) for major page bands; tighter than typical SaaS marketing (80–96px) because marketplace pages need higher card density per scroll.
|
||||
- **Card internal padding:** `{spacing.lg}` (24px) for `{component.host-card}` and `{component.reservation-card}`; `{spacing.base}` (16px) for property-card meta block; `{spacing.sm}` (8px) for caption / date-row gutters.
|
||||
- **Gutters:** `{spacing.base}` (16px) between cards in the homepage city grid; `{spacing.lg}` (24px) inside footer column gutters; `{spacing.xs}` (4px) on dense category-strip dividers.
|
||||
|
||||
### Grid & Container
|
||||
- **Max content width:** ~1280px centered on the homepage and editorial pages. Listing detail pages cap closer to 1080px to keep the photo banner and reservation rail readable.
|
||||
- **City link grid (homepage footer):** 6-column grid at desktop with each cell housing a city name in `{typography.title-md}` and a category sub-label in `{typography.body-sm}` muted.
|
||||
- **Listing detail:** 2-column with photo / amenity body on the left (~64% width) and a sticky reservation card (`{component.reservation-card}`) on the right (~32%).
|
||||
- **Footer:** 3-column link list (Support / Hosting / Airbnb) at desktop, collapsing to 1-column on mobile.
|
||||
|
||||
### Whitespace Philosophy
|
||||
The system gives editorial bands 64px of vertical breathing room but compresses card grids — property and city-link cards sit just 16px apart. The contrast is intentional: the page reads as "open hero, dense marketplace below," reinforcing the marketplace nature without overwhelming the visitor at the fold.
|
||||
|
||||
## Elevation
|
||||
|
||||
The system has essentially **one shadow tier** plus the flat baseline.
|
||||
|
||||
- **Flat (no shadow):** Body, hero, footer, all editorial bands — 95% of surfaces.
|
||||
- **Card hover float:** `box-shadow: rgba(0, 0, 0, 0.02) 0 0 0 1px, rgba(0, 0, 0, 0.04) 0 2px 6px 0, rgba(0, 0, 0, 0.1) 0 4px 8px 0` — applied to property cards on pointer hover, the search bar at rest, and the dropdown menus (account menu, language picker, date picker). This is the single shadow definition in the entire system.
|
||||
- **Modal scrim:** `{colors.scrim}` rendered at 50% opacity — the global modal backdrop. Used on date pickers, login dialogs, language picker.
|
||||
|
||||
There are no progressive elevation tiers — the system either has the one shadow or none. Depth comes from photography, the white-on-white surface separation, and rounded-corner clipping rather than from layered shadows.
|
||||
|
||||
## Components
|
||||
|
||||
### Buttons
|
||||
|
||||
**`button-primary`** — Rausch fill, white text, 8px radius, 14×24px padding, 48px height, weight 500. The most common CTA across the system: "Reserve", "Continue", "Search", account-flow primaries.
|
||||
|
||||
**`button-primary-active`** — The press state. Background flips to `{colors.primary-active}`. No transform, no shadow change.
|
||||
|
||||
**`button-primary-disabled`** — Pale Rausch tint at #ffd1da with white text. Cursor not-allowed.
|
||||
|
||||
**`button-secondary`** — White fill with ink text and a 1px ink outline. 8px radius. Used for "Save", "Cancel", and inverse CTAs over Rausch surfaces.
|
||||
|
||||
**`button-tertiary-text`** — Plain ink text, no surface, no border. Underlined on hover. Used for "Show more" type links and modal close labels.
|
||||
|
||||
**`button-pill-rausch`** — A pill-shaped Rausch CTA used on featured cells (e.g., "Become a host" sub-CTA) — 9999px radius, 10×20px padding, 14px label.
|
||||
|
||||
### Search Surface
|
||||
|
||||
**`search-bar-pill`** — The signature global search bar. White fill, 9999px radius, 64px height, 1px hairline 1px-shadow border. Internally divided by vertical hairline rules into `{component.search-field-segment}` cells (Where / When / Who). Each segment holds an uppercase caption label above a placeholder line in `{typography.caption}`.
|
||||
|
||||
**`search-orb`** — The circular Rausch orb terminating the right edge of the search bar. 48×48px, fully rounded, white magnifying-glass icon centered. The hottest single color moment on the homepage.
|
||||
|
||||
### Top Navigation
|
||||
|
||||
**`top-nav`** — White surface, 80px height, 1px bottom hairline. The Airbnb wordmark sits flush left, the three product tabs (Homes / Experiences / Services) sit in the dead center, and account utilities (host link, language globe, account menu) sit flush right.
|
||||
|
||||
**`product-tab-active`** — Ink label in `{typography.nav-link}`, 32px hand-illustrated icon, 2px ink underline rule beneath the icon-label pair.
|
||||
|
||||
**`product-tab-inactive`** — Muted label, illustrated icon, no underline. Becomes active on click.
|
||||
|
||||
**`new-tag`** — A tiny rounded-pill badge (`{rounded.full}`) anchored top-right of an icon, carrying the uppercase "NEW" label in `{typography.uppercase-tag}` (8px / 700 with 0.32px tracking, uppercase). Used on Experiences and Services to signal recency.
|
||||
|
||||
### Listing Cards
|
||||
|
||||
**`property-card`** — A photo-first card. 1:1 aspect-ratio image with `{rounded.md}` corner clipping, image carousel dots overlay, "Guest favorite" floating badge top-left (`{component.guest-favorite-badge}`), and a heart icon top-right (`{component.icon-button-circle}` in default outlined state, Rausch-filled when saved). Beneath the image: 4–5 lines of meta — title (`{typography.title-md}`), distance / dates (`{typography.body-sm}` muted), and price ("$X night") right-aligned.
|
||||
|
||||
**`property-card-photo`** — The photo plate itself, separated as a token because some surfaces (wishlist, search results) reuse just the photo without the meta block.
|
||||
|
||||
**`experience-card`** — A taller-aspect card (4:5) for experience listings. Same `{rounded.md}` clipping, floating "NEW" badge top-left, heart top-right, and a single-line title beneath.
|
||||
|
||||
**`guest-favorite-badge`** — White rounded pill (`{rounded.full}`) at 11px / 600 weight. Sits over the photo with the system's only shadow tier applied for elevation.
|
||||
|
||||
### Listing Detail
|
||||
|
||||
**`rating-display-card`** — The signature listing-detail moment. A 64px / 700 rating number ("4.81") flanked left and right by tiny laurel-wreath SVG ornaments. Beneath the rating: "Guest favorite" tagline and a row of ink stat columns. The largest typographic weight in the whole system.
|
||||
|
||||
**`amenity-row`** — A 1-column list of amenity icons + ink labels in `{typography.body-md}`. 12px row padding, no border between rows; section is closed by a 1px hairline divider above and below.
|
||||
|
||||
**`reviews-card`** — A 2-column grid of review excerpts. Each column holds an author row (avatar, name, date) above a 3-line excerpt with "Show more" tertiary link.
|
||||
|
||||
**`host-card`** — A white card with `{rounded.md}` rounding and 24px padding holding a host avatar, name, "Superhost" badge, response-rate stat, and a "Contact host" `{component.button-secondary}`.
|
||||
|
||||
**`reservation-card`** — The sticky right-rail card on listing detail pages. White surface, `{rounded.md}` rounding, 1px hairline border, 1px shadow tier elevation, 24px padding. Contains: nightly price (`{typography.display-md}` ink), date-range selector, guest-count stepper, "Reserve" primary CTA full-width, and a fee breakdown stack beneath in `{typography.body-sm}`.
|
||||
|
||||
### Date Picker
|
||||
|
||||
**`date-picker-day`** — A 40×40px circular cell carrying the day number in `{typography.body-sm}`. Default state is transparent fill, ink text.
|
||||
|
||||
**`date-picker-day-selected`** — Ink fill, white text, full circle (`{rounded.full}`). Range states between two selected days carry a `{colors.surface-soft}` lozenge background that connects them.
|
||||
|
||||
### Forms
|
||||
|
||||
**`text-input`** — White surface, 1px hairline outline, `{rounded.sm}` 8px radius, 56px height, 14×12px padding. Stacked label above (in `{typography.caption}` muted), placeholder text in `{typography.body-md}` muted. On focus, the border thickens to 2px ink and the border color flips to `{colors.ink}` — no glow, no ring.
|
||||
|
||||
### Footer
|
||||
|
||||
**`footer-light`** — White surface (matches the page canvas — Airbnb has no contrast footer), 48×80px padding. Three columns of link blocks (Support / Hosting / Airbnb), separated by generous 24px gutters. Each column heads with a `{typography.title-sm}` ink label and stacks `{component.footer-link}` rows in `{typography.body-sm}` ink.
|
||||
|
||||
**`legal-band`** — A bottom strip beneath the footer columns carrying the copyright line, language picker (globe icon + "English (US)" link), currency picker, and social icons (Facebook, X, Instagram). All text in muted `{colors.muted}` at `{typography.caption-sm}`.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
| Name | Width | Key Changes |
|
||||
|---|---|---|
|
||||
| Mobile | < 744px | Top nav collapses to logo + hamburger; product tabs hide behind a sheet; search bar collapses to a single tappable pill; property cards stack 1-up; city grid 1-column; listing detail collapses reservation card to a sticky bottom bar. |
|
||||
| Tablet | 744–1128px | Top nav keeps product tabs but search bar narrows; property cards 2-up; city grid 2–3 column; reservation card stays sticky right-rail at narrower width. |
|
||||
| Desktop | 1128–1440px | Full top nav with three product tabs centered; search bar at full pill width with all 3 segments visible; property cards 4-up; city grid 6-column; listing detail 2-column with reservation rail. |
|
||||
| Wide | > 1440px | Content width caps at 1440px on listing/search pages and ~1280px on editorial; gutters absorb the rest. |
|
||||
|
||||
### Touch Targets
|
||||
- Primary CTAs at minimum 48×48px (above WCAG AAA).
|
||||
- Search orb is 48×48px circular — the most-tapped element on the page.
|
||||
- Heart save button is 32×32px circular — borderline for AAA but compensated by a generous 12px padding inside the photo card.
|
||||
- Date-picker day cells are 40×40px circular.
|
||||
|
||||
### Collapsing Strategy
|
||||
- Top product tabs collapse into a hamburger sheet below 744px.
|
||||
- Search bar's 3 segments collapse into a single-tap entry that opens a full-screen search overlay on mobile.
|
||||
- Property and city-link grids drop column counts cleanly at each breakpoint — never reflow rows; always reduce columns.
|
||||
- Reservation card on listing detail switches from sticky right-rail to a sticky bottom bar on mobile, carrying just the "Reserve" CTA + nightly price summary.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Hover state colors:** intentionally not documented per the global no-hover policy — Airbnb's actual `:hover` styling for property cards is a subtle elevation lift, but precise extraction is unreliable.
|
||||
- **Loading states / skeleton screens:** not visible on the extracted surfaces.
|
||||
- **Map view styling:** the search-results map uses Mapbox-tinted tiles with custom Rausch markers; not captured here.
|
||||
- **Form input error states:** error text color (`{colors.primary-error-text}`) is documented, but the full input outline + helper-text combination on validation failure was not visible in the captured surfaces.
|
||||
- **Sub-brand palettes:** Luxe (`{colors.luxe}`) and Plus (`{colors.plus}`) are documented as tokens, but their full sub-system (typography overrides, surface treatment) lives on separate sub-domains and is not captured here.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Airbnb Inspired Design System
|
||||
|
||||
Design system details have been moved to: https://getdesign.md/airbnb/design-md
|
||||
|
||||
You can also view previews, dark mode examples, and download options on getdesign.md.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,261 @@
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
* design-systems/airbnb/tokens.css
|
||||
*
|
||||
* Structured token bindings for the Airbnb design system — pristine
|
||||
* Canvas White surfaces, a single Rausch coral-pink accent (#ff385c),
|
||||
* generous soft-circle radii, and Airbnb Cereal VF as the lone type
|
||||
* family. The system reads like a travel magazine that happens to be
|
||||
* an app: the interface disappears so the listings can breathe.
|
||||
*
|
||||
* This file pre-compiles the values described in `DESIGN.md` into the
|
||||
* schema shared with every OD design system. Agents should paste the
|
||||
* `:root { … }` block verbatim into the first `<style>` of an artifact,
|
||||
* then resolve every value via `var(--*)` from that point on.
|
||||
*
|
||||
* Schema notes (brand-specific bindings worth flagging — section
|
||||
* references point into `DESIGN.md`):
|
||||
*
|
||||
* #Bind 1 — --bg and --surface both resolve to Canvas White
|
||||
* (#ffffff). Airbnb cards sit directly on the page
|
||||
* without a tonal step; separation comes from the
|
||||
* Hairline Gray (--border) edge and from the
|
||||
* photograph inside the card, never from a surface
|
||||
* tint (§4 Listing Card).
|
||||
*
|
||||
* #Bind 2 — --surface-warm binds to Soft Cloud (#f7f7f7), the
|
||||
* brand-named subsurface tier used for footers, map
|
||||
* wrappers, and the calendar's middle-of-range dates.
|
||||
* A real value rather than `var(--surface)` because
|
||||
* Airbnb publishes a distinct tier here (§2 Surface).
|
||||
*
|
||||
* #Bind 3 — --accent-hover and --accent-active are hand-picked,
|
||||
* NOT formula-derived. DESIGN.md §2 publishes Deep
|
||||
* Rausch (#e00b41) as the pressed/active state.
|
||||
* color-mix would overshoot that saturation and
|
||||
* under-shoot the brand's gradient terminal stop,
|
||||
* so we hand-bind both states.
|
||||
*
|
||||
* #Bind 4 — Radius scale concentrates on the brand's signature
|
||||
* soft-circle geometry: 8px for buttons/inputs, 14px
|
||||
* for listing cards (the workhorse), 20px for the
|
||||
* booking panel and hero photo frames, 9999px for
|
||||
* the circular icon buttons and avatars that
|
||||
* punctuate every surface (§5 Border Radius Scale).
|
||||
*
|
||||
* #Bind 5 — --elev-raised encodes Airbnb's signature three-
|
||||
* layer booking-panel shadow verbatim — a 2% hairline
|
||||
* ring, a 4% short blur, and a 10% medium blur. The
|
||||
* brand forbids single drop shadows; this stacked
|
||||
* triple is what gives the booking panel its premium
|
||||
* anti-aliased perimeter (§6 Depth & Elevation).
|
||||
*
|
||||
* #Bind 6 — --focus-ring is 2px solid Ink Black (#222222), NOT
|
||||
* the schema's accent-tinted default. DESIGN.md §6
|
||||
* mandates this specific ring so focus reads cleanly
|
||||
* against full-bleed colorful photography — a Rausch
|
||||
* ring would disappear against warm sunset shots or
|
||||
* sea-blue overlays. Pair with the 4px white
|
||||
* separator ring at circular buttons floating on
|
||||
* images.
|
||||
*
|
||||
* #Bind 7 — --leading-body is 1.43 (generous). Airbnb pairs
|
||||
* tight display leading (1.18–1.25) with notably
|
||||
* open body leading; that two-mode rhythm is why
|
||||
* the system reads as a travel magazine rather than
|
||||
* a utility app (§3 Principles).
|
||||
*
|
||||
* Brand-specific extensions: none in this revision. The Plus
|
||||
* Magenta (#92174d) and Luxe Purple (#460479) product-tier accents
|
||||
* called out in DESIGN.md §2 stay inline at the tier-specific
|
||||
* components that need them; promoting them to schema slots would
|
||||
* encourage non-tier surfaces to reach for them.
|
||||
* ─────────────────────────────────────────────────────────────────── */
|
||||
|
||||
:root {
|
||||
/* ─── Surface (3 levels) ────────────────────────────────────────
|
||||
* Canvas White is both the page and the card — Airbnb refuses a
|
||||
* tonal shift between the two. Cards earn their edges from the
|
||||
* Hairline Gray border and from full-bleed photography. The
|
||||
* tertiary tier (--surface-warm) is Soft Cloud, used only for
|
||||
* footer backgrounds, map-view wrappers, and middle-of-range
|
||||
* date cells in the date picker. */
|
||||
--bg: #ffffff;
|
||||
--surface: #ffffff;
|
||||
--surface-warm: #f7f7f7;
|
||||
|
||||
/* ─── Foreground ramp (4 levels) ────────────────────────────────
|
||||
* Ink Black (#222222) carries roughly ninety percent of every
|
||||
* page — every heading, every body paragraph, every nav label,
|
||||
* every price. The brand's near-black is never true #000000.
|
||||
* Charcoal (--fg-2) is a one-step-down emphasis tier reserved
|
||||
* for focused-input text. Ash Gray (--muted) is the secondary
|
||||
* label — "Cottage rentals" subtitles, muted footer links.
|
||||
* Mute Gray (--meta) is reserved for disabled buttons and
|
||||
* low-priority metadata. */
|
||||
--fg: #222222;
|
||||
--fg-2: #3f3f3f;
|
||||
--muted: #6a6a6a;
|
||||
--meta: #929292;
|
||||
|
||||
/* ─── Border (2 levels) ─────────────────────────────────────────
|
||||
* Hairline Gray (#dddddd) is the workhorse — every card-to-card
|
||||
* divider, every amenity-row separator, every footer-column
|
||||
* rule. The soft tier is a lighter hairline used to break up
|
||||
* dense list interiors without competing with the primary card
|
||||
* edge. */
|
||||
--border: #dddddd;
|
||||
--border-soft: #ebebeb;
|
||||
|
||||
/* ─── Accent ────────────────────────────────────────────────────
|
||||
* Rausch coral-pink — the single signature color. Reserve for
|
||||
* primary CTAs (Reserve, Search, Add dates), the active-tab
|
||||
* underline, the wishlist heart fill, and price emphasis. Hard
|
||||
* cap of two visible uses per viewport; if a third Rausch
|
||||
* element shows up, neutralize one. Plus Magenta (#92174d) and
|
||||
* Luxe Purple (#460479) are kept inline at their product-tier
|
||||
* surfaces, not promoted to shared accent slots. */
|
||||
--accent: #ff385c;
|
||||
--accent-on: #ffffff;
|
||||
--accent-hover: #e31c5f; /* hand-picked, between Rausch and Deep Rausch */
|
||||
--accent-active: #e00b41; /* Deep Rausch — DESIGN.md §2 pressed-state value */
|
||||
|
||||
/* ─── Semantic ──────────────────────────────────────────────────
|
||||
* Error Red is explicit from DESIGN.md §2; success and warn are
|
||||
* not specified by the brand and are bound to desaturated values
|
||||
* that survive the white canvas without competing with Rausch.
|
||||
* Keep total semantic-color pixels well under five percent of
|
||||
* any page — the brand almost never renders status. */
|
||||
--success: #008a05; /* hand-picked warm green — available/in-stock */
|
||||
--warn: #c47700; /* burnt amber — never tailwind yellow */
|
||||
--danger: #c13515; /* DESIGN.md §2 Error Red */
|
||||
|
||||
/* ─── Typography — fonts ────────────────────────────────────────
|
||||
* Airbnb Cereal VF is the proprietary face that carries every
|
||||
* label from 8px superscript to 28px section heading. The
|
||||
* documented fallback chain renders acceptably on macOS/iOS
|
||||
* where system-ui resolves to San Francisco. Display and body
|
||||
* share the same stack — the visual identity comes from the
|
||||
* family itself, never from typeface mixing. Mono is a generic
|
||||
* stack for kbd, tabular metrics, and the rare code label. */
|
||||
--font-display:
|
||||
"Airbnb Cereal VF", "Airbnb Cereal App", Circular,
|
||||
-apple-system, system-ui, "Helvetica Neue", Roboto, sans-serif;
|
||||
--font-body:
|
||||
"Airbnb Cereal VF", "Airbnb Cereal App", Circular,
|
||||
-apple-system, system-ui, "Helvetica Neue", Roboto, sans-serif;
|
||||
--font-mono:
|
||||
ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Monaco,
|
||||
Consolas, monospace;
|
||||
|
||||
/* ─── Typography — type scale (px) ──────────────────────────────
|
||||
* Derived from DESIGN.md §3 Hierarchy table. The system clusters
|
||||
* tightly between 11 and 22px (the conversational range) and
|
||||
* jumps to 28px for section headings; the higher tiers are used
|
||||
* sparingly for the Guest Favorite rating lockup (44–56px) and
|
||||
* the rare hero numeral. No 400-regular: Cereal's body weight
|
||||
* is 500, which any consuming component should set explicitly. */
|
||||
--text-xs: 12px; /* micro / footer disclaimer */
|
||||
--text-sm: 14px; /* button default, link, caption */
|
||||
--text-base: 16px; /* body medium, button large, subtitle bold */
|
||||
--text-lg: 20px; /* listing title */
|
||||
--text-xl: 22px; /* subsection heading */
|
||||
--text-2xl: 28px; /* section heading */
|
||||
--text-3xl: 44px; /* Guest Favorite rating, hero numeral */
|
||||
--text-4xl: 56px; /* display ceiling — Guest Favorite max */
|
||||
|
||||
/* ─── Typography — leading & tracking ───────────────────────────
|
||||
* Tight for display (1.20 — chiseled headlines), generous for
|
||||
* body (1.43 — magazine reading comfort). Display tracking
|
||||
* compresses negatively at sizes ≥20px; body and caption hold
|
||||
* at zero. The two-mode rhythm is what gives Airbnb pages
|
||||
* their travel-magazine feel rather than a utility-app feel. */
|
||||
--leading-body: 1.43;
|
||||
--leading-tight: 1.2;
|
||||
--tracking-display: -0.02em; /* applied at sizes ≥20px only */
|
||||
|
||||
/* ─── Spacing — base scale ──────────────────────────────────────
|
||||
* 8px base unit per DESIGN.md §5. The brand uses fine-grained
|
||||
* off-grid values (5.5, 10, 11, 18.5, 22) for pixel-perfect
|
||||
* icon alignment, but the shared scale stays on the standard
|
||||
* 4px grid — off-grid spacing belongs inline at the component
|
||||
* level, not in shared tokens. */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-12: 48px;
|
||||
|
||||
/* ─── Section rhythm ────────────────────────────────────────────
|
||||
* Section padding ranges 48–64px on desktop, 24–32px on mobile
|
||||
* per DESIGN.md §5. We bind the upper bound on desktop because
|
||||
* Airbnb's content-dense pages benefit from generous breathing
|
||||
* room between content blocks; the lower phone bound keeps the
|
||||
* vertical rhythm tight on small screens. */
|
||||
--section-y-desktop: 64px;
|
||||
--section-y-tablet: 48px;
|
||||
--section-y-phone: 32px;
|
||||
|
||||
/* ─── Radius ────────────────────────────────────────────────────
|
||||
* The brand's signature soft-circle geometry. 14px (--radius-md)
|
||||
* is the workhorse — every listing card photograph, every
|
||||
* amenity badge, every generic content container lands here.
|
||||
* 20px (--radius-lg) is reserved for the booking panel and hero
|
||||
* photo frames; 8px (--radius-sm) is the small radius for
|
||||
* buttons, dropdowns, and inputs; --radius-pill (9999px) is the
|
||||
* system's signature round geometry, applied to every circular
|
||||
* icon button, every avatar, and the wishlist heart. */
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 20px;
|
||||
--radius-pill: 9999px;
|
||||
|
||||
/* ─── Elevation ─────────────────────────────────────────────────
|
||||
* Airbnb forbids single drop shadows; the system uses stacked
|
||||
* layered shadows or no shadow at all. Listing cards sit flat
|
||||
* on the canvas. Booking panels, modals, and dropdowns use the
|
||||
* signature three-layer stack — a 2% hairline ring, a 4% short
|
||||
* blur, and a 10% medium blur — which reads as one cohesive
|
||||
* lift while giving the perimeter a premium anti-aliased edge. */
|
||||
--elev-flat: none;
|
||||
--elev-ring: 0 0 0 1px var(--border);
|
||||
--elev-raised:
|
||||
rgba(0, 0, 0, 0.02) 0 0 0 1px,
|
||||
rgba(0, 0, 0, 0.04) 0 2px 6px 0,
|
||||
rgba(0, 0, 0, 0.1) 0 4px 8px 0;
|
||||
|
||||
/* ─── Focus ring ────────────────────────────────────────────────
|
||||
* 2px solid Ink Black, NOT the schema's accent-tinted default.
|
||||
* DESIGN.md §6 mandates this exact treatment so focus indicators
|
||||
* read cleanly against full-bleed colorful photography — a
|
||||
* Rausch-tinted ring would disappear against warm sunset shots
|
||||
* or sea-blue beach overlays. Pair with a 4px white separator
|
||||
* ring at circular-icon buttons that float on images. */
|
||||
--focus-ring: 0 0 0 2px var(--fg);
|
||||
|
||||
/* ─── Motion ────────────────────────────────────────────────────
|
||||
* DESIGN.md did not capture transition timings (static
|
||||
* extraction scope). We bind defaults consistent with the
|
||||
* brand's tactile feel: 150ms for micro-states (the famous
|
||||
* `transform: scale(0.92)` pressed-button rebound) and 200ms
|
||||
* for general state changes. Standard easing is the schema
|
||||
* default — short, purposeful, no overshoot. */
|
||||
--motion-fast: 150ms;
|
||||
--motion-base: 200ms;
|
||||
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
||||
|
||||
/* ─── Layout ────────────────────────────────────────────────────
|
||||
* Detail pages cap at 1280px per DESIGN.md §5; the homepage
|
||||
* listing grid lets the canvas breathe to 1760–1920px on
|
||||
* ultra-wide. We bind the detail-page width as the shared
|
||||
* default because that is the geometry agents will most often
|
||||
* generate (rooms, experiences, host profiles). Gutters
|
||||
* tighten progressively: 40px desktop, 24px tablet, 16px
|
||||
* phone per DESIGN.md §8 small-mobile clause. */
|
||||
--container-max: 1280px;
|
||||
--container-gutter-desktop: 40px;
|
||||
--container-gutter-tablet: 24px;
|
||||
--container-gutter-phone: 16px;
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Airtable
|
||||
description: "A sober, editorial workflow-software interface anchored on white canvas and dark-ink type, where brand voltage comes from full-bleed signature cards in coral, dark green, peach, and dark navy that punctuate long-scroll explainer pages. Primary actions use a near-black pill CTA; secondary actions sit in a white outlined button. Type runs Haas Grotesk in modest weights — never bold for its own sake."
|
||||
|
||||
colors:
|
||||
primary: "#181d26"
|
||||
primary-active: "#0d1218"
|
||||
ink: "#181d26"
|
||||
body: "#333840"
|
||||
muted: "#41454d"
|
||||
hairline: "#dddddd"
|
||||
border-strong: "#9297a0"
|
||||
canvas: "#ffffff"
|
||||
surface-soft: "#f8fafc"
|
||||
surface-strong: "#e0e2e6"
|
||||
surface-dark: "#181d26"
|
||||
surface-dark-elevated: "#1d1f25"
|
||||
signature-coral: "#aa2d00"
|
||||
signature-forest: "#0a2e0e"
|
||||
signature-cream: "#f5e9d4"
|
||||
signature-peach: "#fcab79"
|
||||
signature-mint: "#a8d8c4"
|
||||
signature-yellow: "#f4d35e"
|
||||
signature-mustard: "#d9a441"
|
||||
on-primary: "#ffffff"
|
||||
on-dark: "#ffffff"
|
||||
link: "#1b61c9"
|
||||
link-active: "#1a3866"
|
||||
info: "#254fad"
|
||||
info-border: "#458fff"
|
||||
success: "#006400"
|
||||
success-border: "#39bf45"
|
||||
pricing-ink: "#1d1f25"
|
||||
|
||||
typography:
|
||||
display-xl:
|
||||
fontFamily: "Haas Groot Disp, Haas, sans-serif"
|
||||
fontSize: 48px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.1
|
||||
letterSpacing: 0
|
||||
display-lg:
|
||||
fontFamily: "Haas Groot Disp, Haas, sans-serif"
|
||||
fontSize: 40px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.2
|
||||
letterSpacing: 0
|
||||
display-md:
|
||||
fontFamily: "Haas Groot Disp, Haas, sans-serif"
|
||||
fontSize: 32px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.2
|
||||
letterSpacing: 0
|
||||
title-lg:
|
||||
fontFamily: "Haas, sans-serif"
|
||||
fontSize: 24px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.35
|
||||
letterSpacing: 0.12px
|
||||
title-md:
|
||||
fontFamily: "Haas Groot Disp, Haas, sans-serif"
|
||||
fontSize: 20px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.5
|
||||
letterSpacing: 0
|
||||
title-sm:
|
||||
fontFamily: "Haas, sans-serif"
|
||||
fontSize: 18px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.4
|
||||
letterSpacing: 0
|
||||
label-md:
|
||||
fontFamily: "Haas, sans-serif"
|
||||
fontSize: 16px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.4
|
||||
letterSpacing: 0
|
||||
button:
|
||||
fontFamily: "Haas, sans-serif"
|
||||
fontSize: 16px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.4
|
||||
letterSpacing: 0
|
||||
body-md:
|
||||
fontFamily: "Haas, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.25
|
||||
letterSpacing: 0
|
||||
caption:
|
||||
fontFamily: "Haas, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 500
|
||||
lineHeight: 1.35
|
||||
letterSpacing: 0.16px
|
||||
legal:
|
||||
fontFamily: "Haas, sans-serif"
|
||||
fontSize: 13.12px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.2
|
||||
letterSpacing: 0
|
||||
pricing-display:
|
||||
fontFamily: "Inter Display, system-ui, sans-serif"
|
||||
fontSize: 44.8px
|
||||
fontWeight: 475
|
||||
lineHeight: 1.1
|
||||
letterSpacing: 0
|
||||
pricing-section:
|
||||
fontFamily: "Inter Display, system-ui, sans-serif"
|
||||
fontSize: 28px
|
||||
fontWeight: 475
|
||||
lineHeight: 1.2
|
||||
letterSpacing: 0
|
||||
pricing-card-title:
|
||||
fontFamily: "Inter Display, system-ui, sans-serif"
|
||||
fontSize: 20px
|
||||
fontWeight: 475
|
||||
lineHeight: 1.3
|
||||
letterSpacing: 0
|
||||
|
||||
rounded:
|
||||
xs: 2px
|
||||
sm: 6px
|
||||
md: 10px
|
||||
lg: 12px
|
||||
pill: 9999px
|
||||
full: 9999px
|
||||
|
||||
spacing:
|
||||
xxs: 4px
|
||||
xs: 8px
|
||||
sm: 12px
|
||||
md: 16px
|
||||
lg: 24px
|
||||
xl: 32px
|
||||
xxl: 48px
|
||||
section: 96px
|
||||
|
||||
components:
|
||||
button-primary:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.button}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 16px 24px
|
||||
button-primary-active:
|
||||
backgroundColor: "{colors.primary-active}"
|
||||
textColor: "{colors.on-primary}"
|
||||
rounded: "{rounded.lg}"
|
||||
button-secondary:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.button}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 16px 24px
|
||||
button-secondary-on-dark:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.button}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 16px 24px
|
||||
button-legal:
|
||||
backgroundColor: "{colors.link}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.legal}"
|
||||
rounded: "{rounded.xs}"
|
||||
padding: 12px 10px
|
||||
button-icon-circular:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.full}"
|
||||
size: 40px
|
||||
button-pricing-pill:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.pricing-ink}"
|
||||
typography: "{typography.button}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: 12px 24px
|
||||
text-link:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.link}"
|
||||
typography: "{typography.body-md}"
|
||||
top-nav:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
height: 64px
|
||||
hero-band:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.display-lg}"
|
||||
padding: 96px
|
||||
signature-coral-card:
|
||||
backgroundColor: "{colors.signature-coral}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.display-md}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 48px
|
||||
signature-forest-card:
|
||||
backgroundColor: "{colors.signature-forest}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.display-md}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 48px
|
||||
hero-card-dark:
|
||||
backgroundColor: "{colors.surface-dark}"
|
||||
textColor: "{colors.on-dark}"
|
||||
typography: "{typography.display-md}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 48px
|
||||
feature-card-tabbed:
|
||||
backgroundColor: "{colors.surface-soft}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.title-lg}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 32px
|
||||
cream-callout-card:
|
||||
backgroundColor: "{colors.signature-cream}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.title-lg}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: 24px
|
||||
demo-grid-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.label-md}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: 16px
|
||||
logo-strip:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.muted}"
|
||||
typography: "{typography.body-md}"
|
||||
padding: 32px
|
||||
article-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.title-sm}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: 16px
|
||||
topic-filter-rail:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.body}"
|
||||
typography: "{typography.body-md}"
|
||||
width: 240px
|
||||
text-input:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-md}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: 12px 16px
|
||||
height: 44px
|
||||
text-input-focus:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.sm}"
|
||||
pricing-tier-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.pricing-ink}"
|
||||
typography: "{typography.pricing-card-title}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: 32px
|
||||
pricing-tier-card-featured:
|
||||
backgroundColor: "{colors.surface-soft}"
|
||||
textColor: "{colors.pricing-ink}"
|
||||
typography: "{typography.pricing-card-title}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: 32px
|
||||
pricing-comparison-row:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.body}"
|
||||
typography: "{typography.body-md}"
|
||||
padding: 12px
|
||||
cta-band-light:
|
||||
backgroundColor: "{colors.surface-strong}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.display-md}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 48px
|
||||
footer:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.body}"
|
||||
typography: "{typography.body-md}"
|
||||
padding: 64px
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Airtable's marketing surfaces are quietly editorial. The base atmosphere is white canvas, dark ink type, generous whitespace, and a near-black pill CTA — nothing is fighting for attention until a section needs to. The brand voltage doesn't come from gradient washes or accent walls; it comes from **full-bleed signature cards** in `{colors.signature-coral}`, `{colors.signature-forest}`, and `{colors.surface-dark}` that punctuate long-scroll explainer pages every two or three screens. Between those signature bands, the page reads like a print magazine: a headline, supporting copy, a small image cluster, then breathing room.
|
||||
|
||||
Type voice is Haas Grotesk at modest weights (400 for display, 500 for sub-titles and buttons). Display headlines never go bolder than 500 — emphasis comes from size and color contrast, not from weight. Body copy stays at 14px / 400 throughout. The pricing surface runs its own dialect: **Inter Display** at unusual mid-weights (475 / 575) and **pill-shaped buttons** (`{rounded.pill}`) that don't appear on any other page — a deliberate sub-system signaling "this page is about commercial precision."
|
||||
|
||||
**Key Characteristics:**
|
||||
- Primary CTA is `{colors.primary}` (near-black ink) with white text and a `{rounded.lg}` (12px) corner — it reads as confident and final, never decorative.
|
||||
- Secondary CTA is a `{colors.canvas}` button with `{colors.ink}` text and a hairline outline. The two together form Airtable's signature button pair.
|
||||
- Hero is white canvas. There is no atmospheric gradient, no mesh, no background flourish. The brand strength comes from the type and the buttons sitting in clean whitespace.
|
||||
- Brand voltage lives in **signature surface cards**: `{colors.signature-coral}`, `{colors.signature-forest}`, and `{colors.surface-dark}` carry full-bleed product callouts every few screens.
|
||||
- Demo-card grids carry product UI fragments on `{colors.signature-peach}`, `{colors.signature-mint}`, `{colors.signature-cream}` and other warm pastel surfaces.
|
||||
- Section rhythm: white canvas → coral signature card → white body → cream callout band → dark navy CTA → light gray CTA banner → footer. The canvas resets between every signature surface.
|
||||
- Border radius is hierarchical: `{rounded.lg}` (12px) for primary CTAs and large signature cards, `{rounded.md}` (10px) for content cards and demo grids, `{rounded.sm}` (6px) for inputs, `{rounded.full}` for icon buttons. Pricing buttons jump to `{rounded.pill}` to mark themselves as a separate dialect.
|
||||
- Vertical rhythm is `{spacing.section}` (96px) between major bands — universal across every page.
|
||||
|
||||
## Colors
|
||||
|
||||
### Brand & Accent
|
||||
- **Primary** (`{colors.primary}` — #181d26): The dominant brand color. Used for the primary CTA background, h1/h2 display type, and the `{component.surface-dark}` band. Not "blue, then black" — black IS the primary throughout the marketing system.
|
||||
- **Primary Active** (`{colors.primary-active}` — #0d1218): The press state on primary buttons.
|
||||
|
||||
### Surface
|
||||
- **Canvas** (`{colors.canvas}` — #ffffff): The default page surface; the floor of every editorial body.
|
||||
- **Surface Soft** (`{colors.surface-soft}` — #f8fafc): Tabbed feature cards and the featured pricing tier.
|
||||
- **Surface Strong** (`{colors.surface-strong}` — #e0e2e6): The light gray "Start building with Airtable" CTA banner near the footer.
|
||||
- **Surface Dark** (`{colors.surface-dark}` — #181d26): The dark navy CTA cards used mid-page (for example "The path to 10× every person in your organization").
|
||||
- **Surface Dark Elevated** (`{colors.surface-dark-elevated}` — #1d1f25): The articles-page hero base behind the rainbow-stripe overlay.
|
||||
- **Hairline** (`{colors.hairline}` — #dddddd): The 1px border tone for input outlines, table dividers, secondary-button outlines.
|
||||
|
||||
### Text
|
||||
- **Ink** (`{colors.ink}` — #181d26): The strongest text — h1/h2 display type and primary button text-on-light. Same hex as `{colors.primary}` because they are the same role expressed at type and button layers.
|
||||
- **Body** (`{colors.body}` — #333840): The default running-text color.
|
||||
- **Muted** (`{colors.muted}` — #41454d): Footer links, breadcrumbs, captions.
|
||||
- **Border Strong** (`{colors.border-strong}` — #9297a0): The 1px outline color on disabled secondary buttons.
|
||||
- **On Primary / On Dark** (`{colors.on-primary}` — #ffffff): The text color on primary buttons and dark surfaces.
|
||||
|
||||
### Signature Card Surfaces
|
||||
These are the colors that carry Airtable's brand voltage. They appear as full-bleed, full-card surfaces — never as accents on a small element.
|
||||
- **Coral** (`{colors.signature-coral}` — #aa2d00): The largest signature card on the homepage ("Production apps in prototype speed"). Full-bleed dark coral with white type.
|
||||
- **Forest** (`{colors.signature-forest}` — #0a2e0e): A deep-green signature card used in the homepage demo-grid cluster.
|
||||
- **Cream** (`{colors.signature-cream}` — #f5e9d4): The cream callout band ("The path to 10× every person in your organization") — a soft beige surface holding dark type and product UI fragments.
|
||||
- **Peach** (`{colors.signature-peach}` — #fcab79), **Mint** (`{colors.signature-mint}` — #a8d8c4), **Yellow** (`{colors.signature-yellow}` — #f4d35e), **Mustard** (`{colors.signature-mustard}` — #d9a441): Demo-card surfaces that carry small product UI fragments inside the multi-card grid sections.
|
||||
|
||||
### Semantic
|
||||
- **Link** (`{colors.link}` — #1b61c9): Inline body links and anchor text. Darker on press to `{colors.link-active}` (#1a3866). Despite the `--theme_button-background-primary` CSS-variable name, this color is **not** the primary button color — it is the link color.
|
||||
- **Info** (`{colors.info}` — #254fad) and **Info Border** (`{colors.info-border}` — #458fff): Inline info badges and focused-input outline.
|
||||
- **Success** (`{colors.success}` — #006400) and **Success Border** (`{colors.success-border}` — #39bf45): Confirmation states.
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Family
|
||||
The system runs **Haas / Haas Groot Disp** (Airtable's licensed display + text type). Haas Groot Disp covers display sizes (h1 / h2); Haas Grotesk covers everything 24px and below. The fallback stack walks `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif`.
|
||||
|
||||
The pricing surface runs a separate **Inter Display** stack at mid-weights (475 / 575) — a deliberate sub-system signaling commercial precision.
|
||||
|
||||
### Hierarchy
|
||||
|
||||
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|
||||
|---|---|---|---|---|---|
|
||||
| `{typography.display-xl}` | 48px | 500 | 1.1 | 0 | Articles page h2 — second-tier editorial headline |
|
||||
| `{typography.display-lg}` | 40px | 400 | 1.2 | 0 | Homepage h1 hero |
|
||||
| `{typography.display-md}` | 32px | 400 | 1.2 | 0 | Platform-page h2 — feature-section headlines |
|
||||
| `{typography.title-lg}` | 24px | 400 | 1.35 | 0.12px | Section titles |
|
||||
| `{typography.title-md}` | 20px | 400 | 1.5 | 0 | Sub-section titles in tabbed feature cards |
|
||||
| `{typography.title-sm}` | 18px | 500 | 1.4 | 0 | Article-card titles |
|
||||
| `{typography.label-md}` | 16px | 500 | 1.4 | 0 | Demo-card titles, list labels |
|
||||
| `{typography.button}` | 16px | 500 | 1.4 | 0 | Standard CTA button labels |
|
||||
| `{typography.body-md}` | 14px | 400 | 1.25 | 0 | Body copy, footer links, top-nav items |
|
||||
| `{typography.caption}` | 14px | 500 | 1.35 | 0.16px | Light captions and meta text |
|
||||
| `{typography.legal}` | 13.12px | 600 | 1.2 | 0 | Cookie/legal CTA buttons |
|
||||
| `{typography.pricing-display}` | 44.8px | 475 | 1.1 | 0 | Pricing-page h1 |
|
||||
| `{typography.pricing-section}` | 28px | 475 | 1.2 | 0 | Pricing-page section heads |
|
||||
| `{typography.pricing-card-title}` | 20px | 475 | 1.3 | 0 | Pricing tier card plan name |
|
||||
|
||||
### Principles
|
||||
The Haas system prefers weight 400 for display sizes — a 40px h1 is **not** bold. Visual emphasis is delegated to size, color contrast, and the signature surface cards. Where the system does want weight, it pivots to 500 (sub-titles, buttons, article titles), never 600 or 700 in the editorial body. The only true bold (600) lives in `{typography.legal}` — a sign that boldness is reserved for terms-of-service surfaces, not marketing.
|
||||
|
||||
The pricing-page sub-system uses Inter Display at `font-weight: 475` — a custom mid-weight between regular (400) and medium (500), shipped as a variable font.
|
||||
|
||||
### Note on Font Substitutes
|
||||
If Haas Groot Disp and Haas Grotesk are unavailable, **Inter Display** (variable) is the closest open-source substitute for both — adjust line-height down by ~5% to match Haas's tighter cap-height. For the pricing sub-system, use Inter Display directly. On macOS / iOS, **system-ui** is sufficient; on Windows, the chain falls through to Segoe UI, which is a usable but slightly cooler substitute.
|
||||
|
||||
## Layout
|
||||
|
||||
### Spacing System
|
||||
- **Base unit:** 4px (all spacing snaps to 4-multiples).
|
||||
- **Tokens:** `{spacing.xxs}` 4px · `{spacing.xs}` 8px · `{spacing.sm}` 12px · `{spacing.md}` 16px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 96px.
|
||||
- **Section padding (vertical):** `{spacing.section}` (96px) is the universal vertical rhythm constant — every major editorial band on every page uses 96px top + 96px bottom internal padding.
|
||||
- **Card internal padding:** `{spacing.xl}` (32px) for tabbed feature cards and pricing tier cards; `{spacing.xxl}` (48px) inside signature coral / forest / dark cards; `{spacing.lg}` (24px) for cream callouts and demo-grid cards.
|
||||
- **Gutters:** `{spacing.lg}` (24px) between cards in 3-up grids; `{spacing.md}` (16px) inside denser logo strips and footer column gutters.
|
||||
|
||||
### Grid & Container
|
||||
- **Max content width:** ~1280px centered, with `{spacing.xxl}` (48px) horizontal breathing room.
|
||||
- **Editorial body:** Single 8/12-column at large breakpoints, collapsing to single-column on mobile.
|
||||
- **Demo-card grids:** 3 or 4 columns at desktop, 2 at tablet, 1 at mobile. Card sizes are deliberately uneven within the grid to dodge a uniform "spec sheet" feel.
|
||||
- **Logo strip:** 6 monochrome partner logos in a single row at desktop; wraps to 3-up on mobile.
|
||||
|
||||
### Whitespace Philosophy
|
||||
Airtable uses whitespace as the dominant atmospheric tool. Hero sections sit in 96px+ of pure whitespace above and below the headline + sub-headline pair, with no decoration in that whitespace. The hero is intentionally calm — there is no gradient, no aurora, no atmospheric mesh behind the type. The system trusts whitespace alone to do the framing.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
| Level | Treatment | Use |
|
||||
|---|---|---|
|
||||
| Flat | No shadow, no border | Body sections, top nav, footer |
|
||||
| Soft hairline | 1px `{colors.hairline}` border | Inputs, sub-nav rails, comparison-table dividers, secondary buttons |
|
||||
| Button rest | Soft drop with subtle blue-tinted glow at low alpha | Primary CTA buttons (the blue tint is a holdover from the link color and reads as a faint accent under the dark button) |
|
||||
| Button focus | Outer 2px blue ring at higher alpha | Keyboard focus state on primary buttons |
|
||||
| Card flat | No shadow; relies on color contrast against the surface band | Signature coral / forest / dark cards, cream callouts, demo-grid cards |
|
||||
|
||||
The elevation philosophy is **color-block first, shadow second**. Shadows are minimal; depth is delegated to the contrast between white canvas and signature surface cards. There is no soft-glow / atmospheric-shadow / heavy-elevation language anywhere in the marketing system.
|
||||
|
||||
### Decorative Depth
|
||||
- **Vertical rainbow stripes** appear on the articles hero only — multi-color vertical bands sitting on `{colors.surface-dark-elevated}`. This is a single-page treatment, not a system-wide signature.
|
||||
- **Photography-as-depth** in the demo-card grid: every card carries a real product UI screenshot or mockup, contributing depth through legible artifact density rather than decorative effects.
|
||||
|
||||
## Shapes
|
||||
|
||||
### Border Radius Scale
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `{rounded.xs}` | 2px | Cookie-consent and legal CTA buttons — system-required surfaces |
|
||||
| `{rounded.sm}` | 6px | Text inputs, small inline buttons |
|
||||
| `{rounded.md}` | 10px | Secondary content cards, article cards, cream callouts |
|
||||
| `{rounded.lg}` | 12px | Primary CTA buttons, signature surface cards, tabbed feature cards |
|
||||
| `{rounded.pill}` | 9999px | Pricing-page CTA buttons (sub-system only) |
|
||||
| `{rounded.full}` | 9999px / 50% | Circular icon buttons, avatar surfaces |
|
||||
|
||||
### Photography Geometry
|
||||
Product UI screenshots inside demo-card grids retain native aspect ratios (typically 4:3 or 16:10) and crop into `{rounded.md}` containers. Hero illustrations bleed full-width with no rounding. Article-card thumbnails use 16:9 with `{rounded.md}` corners. Avatars in testimonials use `{rounded.full}` (perfect circles). Pricing comparison table images stay rectangular with no rounding.
|
||||
|
||||
## Components
|
||||
|
||||
> **No hover states documented.** Per the global no-hover policy (Step 6), every component spec below documents only Default and Active/Pressed states. Variants live as separate entries in the `components:` front matter.
|
||||
|
||||
**`top-nav`** — A 64px-tall white bar pinned to the top of every page. Airtable wordmark sits at left; primary horizontal menu (Platform, Solutions, Resources, Enterprise, Pricing) sits center-left in `{typography.body-md}`; the right cluster carries a "Book Demo" outline link, "Sign up for free" `{component.button-primary}`, and "Log In" text link. The nav stays light on every page — Airtable does not invert the nav over dark sections.
|
||||
|
||||
### Buttons
|
||||
|
||||
**`button-primary`** — The signature primary CTA. Background `{colors.primary}` (near-black), text `{colors.on-primary}`, type `{typography.button}`, padding 16px × 24px, rounded `{rounded.lg}` (12px). This is the "Get started for free" / "Sign up for free" button visible on every hero. It reads as confident and final — not decorative — which is why the system uses it sparingly (one per viewport).
|
||||
- Active state: `button-primary-active` darkens to `{colors.primary-active}` (#0d1218).
|
||||
|
||||
**`button-secondary`** — White outline button (e.g. "Book demo"). Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.button}`, rounded `{rounded.lg}` (12px), 1px hairline outline. Sits next to `{component.button-primary}` as the "less-committed" choice.
|
||||
|
||||
**`button-secondary-on-dark`** — Same shape as `{component.button-secondary}` but used on signature coral / forest / dark surfaces. Background `{colors.canvas}`, text `{colors.ink}` — the white button stays white over dark surfaces because the system never inverts to a translucent on-dark style on the marketing site.
|
||||
|
||||
**`button-pricing-pill`** — The pricing-page CTA family. Background `{colors.canvas}`, text `{colors.pricing-ink}`, rounded `{rounded.pill}` (9999px), padding 12px × 24px. The only place pill-shape appears in the marketing system. Treat it as part of the pricing sub-system signaling.
|
||||
|
||||
**`button-legal`** — Cookie-consent and legal-banner CTAs. Background `{colors.link}`, text `{colors.on-primary}`, type `{typography.legal}` (13.12px / 600), rounded `{rounded.xs}` (2px), padding 12px × 10px. The 2px corner radius and 600 weight signal "this is a required system surface," not a designed brand surface.
|
||||
|
||||
**`button-icon-circular`** — 40px × 40px circular button with `{colors.canvas}` background, hairline border, and `{colors.ink}` icon. Used for carousel controls, "share", and "back" affordances.
|
||||
|
||||
**`text-link`** — Inline body links in `{colors.link}` (#1b61c9, the actual link blue). No underline by default. Type inherits `{typography.body-md}`.
|
||||
|
||||
### Cards & Containers
|
||||
|
||||
**`hero-band`** — The full-page-width white-canvas hero. No surface card, no border, no shadow, no atmospheric gradient — just the headline, sub-headline, and primary + secondary button pair sitting in 96px of whitespace. Vertical padding `{spacing.section}` (96px).
|
||||
|
||||
**`signature-coral-card`** — The large full-bleed coral card on the homepage ("Production apps in prototype speed"). Background `{colors.signature-coral}` (#aa2d00, a dark coral / oxide red), text `{colors.on-primary}`, rounded `{rounded.lg}` (12px), internal padding `{spacing.xxl}` (48px). Carries an h2 in `{typography.display-md}`, supporting copy in `{typography.body-md}`, and `{component.button-secondary-on-dark}` as the CTA.
|
||||
|
||||
**`signature-forest-card`** — A deep green signature card (`{colors.signature-forest}` — #0a2e0e) used as a demo-grid sibling to the coral card on the homepage.
|
||||
|
||||
**`hero-card-dark`** — The dark navy mid-page CTA card (e.g. "The path to 10× every person in your organization"). Background `{colors.surface-dark}` (#181d26), text `{colors.on-dark}`, rounded `{rounded.lg}` (12px), internal padding `{spacing.xxl}` (48px). The same color as `{colors.primary}` because the system uses ink as both type color and signature dark surface.
|
||||
|
||||
**`feature-card-tabbed`** — Light-cream cards (e.g. the "Coke / Pelosi / Conde Nast / Time Inc" tabbed feature card on the homepage). Background `{colors.surface-soft}`, rounded `{rounded.lg}` (12px), internal padding `{spacing.xl}` (32px). Left rail carries vertically-stacked tab labels in `{typography.title-md}`; right pane shows the active tab's content (illustration + body copy + small CTA).
|
||||
|
||||
**`cream-callout-card`** — Beige callout cards (`{colors.signature-cream}`). Rounded `{rounded.md}` (10px), internal padding `{spacing.lg}` (24px). Carry product UI fragments or stat callouts — softer than the dark/coral signature cards but still a deliberate brand surface.
|
||||
|
||||
**`demo-grid-card`** — Used in multi-card grids that punctuate every page. Background `{colors.canvas}` or one of the demo-grid surfaces (`{colors.signature-peach}`, `{colors.signature-mint}`, `{colors.signature-yellow}`, `{colors.signature-mustard}`), rounded `{rounded.md}` (10px), internal padding `{spacing.md}` (16px). Each card frames a product UI fragment. Card heights vary deliberately to dodge a uniform "spec sheet" feel.
|
||||
|
||||
**`logo-strip`** — Horizontal monochrome partner-logo row (HBO, Netflix, Amazon, Time, Conde Nast). Logos render in `{colors.muted}`, surface is `{colors.canvas}`, vertical padding `{spacing.xl}` (32px). 6 logos at desktop, 3 at mobile.
|
||||
|
||||
**`article-card`** — The trending-stories grid on the articles page. Background `{colors.canvas}`, rounded `{rounded.md}` (10px), internal padding `{spacing.md}` (16px). Each card carries a colorful illustrated thumbnail (16:9), a small uppercase category tag, an `{typography.title-sm}` title, and a meta line. 3-up at desktop.
|
||||
|
||||
**`topic-filter-rail`** — The left rail on the articles page. 240px wide, `{colors.canvas}` background, `{typography.body-md}`, vertically grouped category headings ("Marketing", "Product", "Project management", "Operations") with sub-bullets. Active item carries a small numeric count badge.
|
||||
|
||||
### Inputs & Forms
|
||||
|
||||
**`text-input`** — Standard text input. Background `{colors.canvas}`, text `{colors.ink}`, type `{typography.body-md}`, rounded `{rounded.sm}` (6px), padding 12px × 16px, height 44px. 1px hairline border in `{colors.hairline}`.
|
||||
|
||||
**`text-input-focus`** — Focus state. Border thickens or recolors to `{colors.info-border}`.
|
||||
|
||||
### Pricing Sub-System
|
||||
|
||||
**`pricing-tier-card`** — Standard tier card. Background `{colors.canvas}`, text `{colors.pricing-ink}`, type `{typography.pricing-card-title}` for the plan name, rounded `{rounded.md}` (10px), internal padding `{spacing.xl}` (32px). Carries the plan name, a price block in `{typography.pricing-display}` (44.8px / 475), feature checklist, and a `{component.button-pricing-pill}` at the bottom.
|
||||
|
||||
**`pricing-tier-card-featured`** — The featured tier (typically "Team" or "Business"). Background shifts to `{colors.surface-soft}`. No accent border, no badge — the background tone shift is the only signal.
|
||||
|
||||
**`pricing-comparison-row`** — Each row of the long comparison table at the bottom of the pricing page. Labels in the left column; checkmarks or values across 4 plan columns. 12px vertical padding per row, hairline divider between rows.
|
||||
|
||||
### Navigation Variants
|
||||
|
||||
**`footer`** — Light surface (`{colors.canvas}`), 6-column link list at desktop covering Platform / Solutions / Resources / Learn / Company sub-trees. Vertical padding `{spacing.section}` divided across upper link block and lower legal row. Type `{typography.body-md}`.
|
||||
|
||||
**`cta-band-light`** — The light gray "Start building with Airtable" CTA strip near the footer. Background `{colors.surface-strong}` (#e0e2e6), text `{colors.ink}`, rounded `{rounded.lg}` (12px), padding `{spacing.xxl}` (48px). Carries an h2 in `{typography.display-md}` and a `{component.button-primary}`.
|
||||
|
||||
### Signature Components
|
||||
|
||||
**Articles Vertical Rainbow Stripe Hero** — The articles-page hero treatment. Multi-color vertical bands at varying widths sitting on `{colors.surface-dark-elevated}`. The h1 + sub-head + CTA cluster sits center-left on top of the stripes. This is a single-page hero treatment, not a system-wide signature — do not promote it to a multi-page pattern.
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do
|
||||
- Keep `{component.button-primary}` near-black. The brand's primary CTA is `{colors.primary}`, not the link blue. Mixing them up turns a confident hero into a confused one.
|
||||
- Reserve `{component.button-primary}` for one primary action per viewport. The system is designed for scarcity at the brand-action layer.
|
||||
- Use `{component.button-secondary}` (white with hairline outline) as the natural pair with `{component.button-primary}`. The two together form Airtable's signature button row.
|
||||
- Trust whitespace as the hero atmosphere. Hero bands are intentionally calm — no gradient, no mesh, no atmospheric backdrop. Going against this reads as off-brand.
|
||||
- Use `{component.signature-coral-card}`, `{component.signature-forest-card}`, and `{component.hero-card-dark}` to break editorial monotony. These are the brand's voltage moments.
|
||||
- Keep `{component.demo-grid-card}` heights uneven within a grid. Uniform heights feel like a spec sheet.
|
||||
- Treat the pricing surface as its own dialect: keep `{typography.pricing-display}`, `{typography.pricing-card-title}`, and `{component.button-pricing-pill}` together. Mixing them with Haas Grotesk button type breaks the sub-system's voice.
|
||||
- Anchor every editorial band with `{spacing.section}` (96px) vertical padding.
|
||||
|
||||
### Don't
|
||||
- Don't make `{colors.link}` (#1b61c9) the primary button color. It is the link color. The primary button is `{colors.primary}` (#181d26, near-black). Treating link-blue as the brand action is the most common mistake when reading Airtable's CSS variables.
|
||||
- Don't add a gradient backdrop to the hero. Airtable's hero is white, full stop. Mesh, aurora, spotlight gradients all read as "another SaaS template" — not Airtable.
|
||||
- Don't bold display-weight type. `{typography.display-xl}` and `{typography.display-lg}` are intentionally weight 400 / 500 — going to 700 reads as marketing-page-template.
|
||||
- Don't use `{rounded.pill}` outside the pricing surface. It's a sub-system signal, not a general radius option.
|
||||
- Don't repeat the same surface mode in two consecutive bands. The editorial pacing depends on rhythm: white → signature card → white → cream → dark → white. Two whites in a row read as a typography blog.
|
||||
- Don't add hover state styling beyond what the system already encodes. The system documents Default and Active/Pressed only.
|
||||
- Don't introduce additional accent colors beyond the documented signature card palette. The system's voltage already uses coral, forest, dark navy, cream, peach, mint, yellow, and mustard.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Breakpoints
|
||||
|
||||
| Name | Width | Key Changes |
|
||||
|---|---|---|
|
||||
| Mobile | < 768px | Single-column body; top nav collapses to hamburger; demo-grid drops to 1-up; signature cards stay full-bleed; logo strip wraps to 2 rows; footer collapses to single-column |
|
||||
| Tablet | 768–1024px | 2-up demo-grid; top nav stays horizontal but tightens; cream-callout cards stack 2-up; pricing comparison table becomes horizontally scrollable |
|
||||
| Desktop | 1024–1440px | 3-up demo-grid (and 4-up for tighter content); full top-nav with all menu items visible; pricing tier cards render 4-across |
|
||||
| Wide | > 1440px | Same as Desktop with more outer breathing room; max content width caps at ~1280px and the page adds outer margin rather than scaling type up |
|
||||
|
||||
### Touch Targets
|
||||
- `{component.button-primary}` and siblings render at 48 × 48px minimum (16px vertical padding + 16px line-height) — comfortably above WCAG AAA's 44 × 44.
|
||||
- `{component.button-icon-circular}` is exactly 40 × 40px — slightly under WCAG's recommended 44, but the centered icon and dot-radius compensate visually.
|
||||
- `{component.text-input}` height is 44px.
|
||||
|
||||
### Collapsing Strategy
|
||||
- Top nav collapses to a hamburger at < 768px; the menu opens as a full-screen sheet rather than a dropdown.
|
||||
- Card grids reduce columns rather than scaling cards down.
|
||||
- The `{component.feature-card-tabbed}` re-stacks the tab rail above the content pane on mobile.
|
||||
- The pricing comparison table converts to horizontally-scrollable swipe at < 1024px; the four plan headers stay visible while body rows scroll.
|
||||
|
||||
### Image Behavior
|
||||
- Demo-card UI screenshots crop to fit their container rather than scaling up.
|
||||
- Hero illustrations bleed full-width on mobile, losing horizontal margin.
|
||||
- Signature card images (inside coral / forest / dark cards) compress to their card width without cropping.
|
||||
|
||||
## Iteration Guide
|
||||
|
||||
1. Focus on ONE component at a time. Reference its YAML key directly (`{component.button-primary}`, `{component.signature-coral-card}`).
|
||||
2. When adding a new component, decide first which sub-system it belongs to: the main editorial system (Haas, `{rounded.lg}`/`{rounded.md}`) or the pricing sub-system (Inter Display, `{rounded.pill}`).
|
||||
3. Variants of an existing component (`-active`, `-disabled`, `-focus`) live as separate entries in `components:` — never as nested state objects.
|
||||
4. Use `{token.refs}` everywhere prose mentions a color, a radius, a typography role, or a spacing value. Hex codes appear at most once next to the reference.
|
||||
5. Never document hover. The system documents Default and Active/Pressed states only.
|
||||
6. Run `npx @google/design.md lint DESIGN.md` after edits — `broken-ref`, `contrast-ratio`, and `orphaned-tokens` warnings flag issues automatically.
|
||||
7. When in doubt about emphasis: bigger type before bolder type, signature surface card before solid accent.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- The exact hex values of pastel demo-grid surfaces (`{colors.signature-peach}`, `{colors.signature-mint}`, `{colors.signature-yellow}`, `{colors.signature-mustard}`) are inferred from screenshot pixel sampling. Some product launches may swap these surfaces seasonally.
|
||||
- Hover behavior across all components is not documented (per global no-hover policy).
|
||||
- Animation and transition timings are not in scope.
|
||||
- Form validation states beyond `text-input-focus` are not extracted — error and success states for inputs would need a dedicated form page to confirm.
|
||||
- The pricing comparison table's checkmark glyph and column-divider widths are described structurally but not formalized as tokens.
|
||||
- The CSS variable `--theme_button-background-primary: #1b61c9` exists at `:root` but is not used as the primary CTA color anywhere on the marketing site. It maps to the link/info color role instead. Documented here so future extractions don't re-trip over the misleading variable name.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Airtable Inspired Design System
|
||||
|
||||
Design system details have been moved to: https://getdesign.md/airtable/design-md
|
||||
|
||||
You can also view previews, dark mode examples, and download options on getdesign.md.
|
||||
@@ -0,0 +1,562 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Apple
|
||||
description: "A photography-first interface that turns marketing into a museum gallery. Edge-to-edge product tiles alternate light and dark canvases, framed by SF Pro Display headlines with negative letter-spacing and a single Action Blue (#0066cc) interactive color. UI chrome recedes so the product can speak — no decorative gradients, no shadows on chrome, only the one signature drop-shadow under product imagery resting on a surface."
|
||||
|
||||
colors:
|
||||
primary: "#0066cc"
|
||||
primary-focus: "#0071e3"
|
||||
primary-on-dark: "#2997ff"
|
||||
ink: "#1d1d1f"
|
||||
body: "#1d1d1f"
|
||||
body-on-dark: "#ffffff"
|
||||
body-muted: "#cccccc"
|
||||
ink-muted-80: "#333333"
|
||||
ink-muted-48: "#7a7a7a"
|
||||
divider-soft: "#f0f0f0"
|
||||
hairline: "#e0e0e0"
|
||||
canvas: "#ffffff"
|
||||
canvas-parchment: "#f5f5f7"
|
||||
surface-pearl: "#fafafc"
|
||||
surface-tile-1: "#272729"
|
||||
surface-tile-2: "#2a2a2c"
|
||||
surface-tile-3: "#252527"
|
||||
surface-black: "#000000"
|
||||
surface-chip-translucent: "#d2d2d7"
|
||||
on-primary: "#ffffff"
|
||||
on-dark: "#ffffff"
|
||||
|
||||
typography:
|
||||
hero-display:
|
||||
fontFamily: "SF Pro Display, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 56px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.07
|
||||
letterSpacing: -0.28px
|
||||
display-lg:
|
||||
fontFamily: "SF Pro Display, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 40px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.1
|
||||
letterSpacing: 0
|
||||
display-md:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 34px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.47
|
||||
letterSpacing: -0.374px
|
||||
lead:
|
||||
fontFamily: "SF Pro Display, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 28px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.14
|
||||
letterSpacing: 0.196px
|
||||
lead-airy:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 24px
|
||||
fontWeight: 300
|
||||
lineHeight: 1.5
|
||||
letterSpacing: 0
|
||||
tagline:
|
||||
fontFamily: "SF Pro Display, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 21px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.19
|
||||
letterSpacing: 0.231px
|
||||
body-strong:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 17px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.24
|
||||
letterSpacing: -0.374px
|
||||
body:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 17px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.47
|
||||
letterSpacing: -0.374px
|
||||
dense-link:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 17px
|
||||
fontWeight: 400
|
||||
lineHeight: 2.41
|
||||
letterSpacing: 0
|
||||
caption:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.43
|
||||
letterSpacing: -0.224px
|
||||
caption-strong:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 600
|
||||
lineHeight: 1.29
|
||||
letterSpacing: -0.224px
|
||||
button-large:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 18px
|
||||
fontWeight: 300
|
||||
lineHeight: 1.0
|
||||
letterSpacing: 0
|
||||
button-utility:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 14px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.29
|
||||
letterSpacing: -0.224px
|
||||
fine-print:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 12px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.0
|
||||
letterSpacing: -0.12px
|
||||
micro-legal:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 10px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.3
|
||||
letterSpacing: -0.08px
|
||||
nav-link:
|
||||
fontFamily: "SF Pro Text, system-ui, -apple-system, sans-serif"
|
||||
fontSize: 12px
|
||||
fontWeight: 400
|
||||
lineHeight: 1.0
|
||||
letterSpacing: -0.12px
|
||||
|
||||
rounded:
|
||||
none: 0px
|
||||
xs: 5px
|
||||
sm: 8px
|
||||
md: 11px
|
||||
lg: 18px
|
||||
pill: 9999px
|
||||
full: 9999px
|
||||
|
||||
spacing:
|
||||
xxs: 4px
|
||||
xs: 8px
|
||||
sm: 12px
|
||||
md: 17px
|
||||
lg: 24px
|
||||
xl: 32px
|
||||
xxl: 48px
|
||||
section: 80px
|
||||
|
||||
components:
|
||||
button-primary:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.body}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: 11px 22px
|
||||
button-primary-focus:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
rounded: "{rounded.pill}"
|
||||
button-primary-active:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
rounded: "{rounded.pill}"
|
||||
button-secondary-pill:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.primary}"
|
||||
typography: "{typography.body}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: 11px 22px
|
||||
button-dark-utility:
|
||||
backgroundColor: "{colors.ink}"
|
||||
textColor: "{colors.on-dark}"
|
||||
typography: "{typography.button-utility}"
|
||||
rounded: "{rounded.sm}"
|
||||
padding: 8px 15px
|
||||
button-pearl-capsule:
|
||||
backgroundColor: "{colors.surface-pearl}"
|
||||
textColor: "{colors.ink-muted-80}"
|
||||
typography: "{typography.caption}"
|
||||
rounded: "{rounded.md}"
|
||||
padding: 8px 14px
|
||||
button-store-hero:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.on-primary}"
|
||||
typography: "{typography.button-large}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: 14px 28px
|
||||
button-icon-circular:
|
||||
backgroundColor: "{colors.surface-chip-translucent}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.full}"
|
||||
size: 44px
|
||||
text-link:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.primary}"
|
||||
typography: "{typography.body}"
|
||||
text-link-on-dark:
|
||||
backgroundColor: transparent
|
||||
textColor: "{colors.primary-on-dark}"
|
||||
typography: "{typography.body}"
|
||||
global-nav:
|
||||
backgroundColor: "{colors.surface-black}"
|
||||
textColor: "{colors.on-dark}"
|
||||
typography: "{typography.nav-link}"
|
||||
height: 44px
|
||||
sub-nav-frosted:
|
||||
backgroundColor: "{colors.canvas-parchment}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.tagline}"
|
||||
height: 52px
|
||||
product-tile-light:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.display-lg}"
|
||||
rounded: "{rounded.none}"
|
||||
padding: 80px
|
||||
product-tile-parchment:
|
||||
backgroundColor: "{colors.canvas-parchment}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.display-lg}"
|
||||
rounded: "{rounded.none}"
|
||||
padding: 80px
|
||||
product-tile-dark:
|
||||
backgroundColor: "{colors.surface-tile-1}"
|
||||
textColor: "{colors.on-dark}"
|
||||
typography: "{typography.display-lg}"
|
||||
rounded: "{rounded.none}"
|
||||
padding: 80px
|
||||
product-tile-dark-2:
|
||||
backgroundColor: "{colors.surface-tile-2}"
|
||||
textColor: "{colors.on-dark}"
|
||||
rounded: "{rounded.none}"
|
||||
product-tile-dark-3:
|
||||
backgroundColor: "{colors.surface-tile-3}"
|
||||
textColor: "{colors.on-dark}"
|
||||
rounded: "{rounded.none}"
|
||||
store-utility-card:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body-strong}"
|
||||
rounded: "{rounded.lg}"
|
||||
padding: 24px
|
||||
configurator-option-chip:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.caption}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: 12px 16px
|
||||
configurator-option-chip-selected:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "{rounded.pill}"
|
||||
search-input:
|
||||
backgroundColor: "{colors.canvas}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body}"
|
||||
rounded: "{rounded.pill}"
|
||||
padding: 12px 20px
|
||||
height: 44px
|
||||
floating-sticky-bar:
|
||||
backgroundColor: "{colors.canvas-parchment}"
|
||||
textColor: "{colors.ink}"
|
||||
typography: "{typography.body}"
|
||||
height: 64px
|
||||
padding: 12px 32px
|
||||
environment-quote-card:
|
||||
backgroundColor: "{colors.surface-tile-1}"
|
||||
textColor: "{colors.on-dark}"
|
||||
typography: "{typography.display-lg}"
|
||||
rounded: "{rounded.none}"
|
||||
padding: 80px
|
||||
footer:
|
||||
backgroundColor: "{colors.canvas-parchment}"
|
||||
textColor: "{colors.ink-muted-80}"
|
||||
typography: "{typography.fine-print}"
|
||||
padding: 64px
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Apple's web presence is a masterclass in **reverent product photography framed by near-invisible UI**. Every page is a stack of edge-to-edge product "tiles" — alternating light and dark canvases, each centered on a hero headline, a one-line tagline, two tiny blue pill CTAs, and an impossibly crisp product render. Nothing competes with the product. Typography is confident but quiet; color is either pure white, an off-white parchment, or a near-black tile; interactive elements are a single, quiet blue.
|
||||
|
||||
Density is unusually low even by contemporary SaaS standards. Each tile occupies roughly one viewport, and there is no decorative chrome — no borders, no gradients, no decorative frames, no shadows on headlines. Elevation appears only when a product image rests on a surface (a single soft `rgba(0, 0, 0, 0.22) 3px 5px 30px` drop for visual weight). The result is a catalog that feels more like a museum gallery: the wall disappears and the artifact takes over.
|
||||
|
||||
Store and shop surfaces retain the same chassis but switch modes. The product configurator (iPhone 17 Pro, accessories grid) introduces a tight grid of white utility cards at `{rounded.lg}` (18px) radius with a thin border, paired with a persistent thin sub-nav strip. The environment page leans darker and more editorial. Across all five surfaces the typographic system, spacing rhythm, and the single blue accent are consistent — this is one design language expressed at different volumes.
|
||||
|
||||
**Key Characteristics:**
|
||||
- Photography-first presentation; UI recedes so the product can speak.
|
||||
- Alternating full-bleed tile sections: white/parchment ↔ near-black, with the color change itself acting as the section divider.
|
||||
- Single blue accent (`{colors.primary}` — #0066cc) carries every interactive element. No second brand color exists.
|
||||
- Two button grammars: tiny blue pill CTAs (`{rounded.pill}`) and compact utility rects (`{rounded.sm}`).
|
||||
- SF Pro Display + SF Pro Text — negative letter-spacing at display sizes for the signature "Apple tight" headline feel.
|
||||
- Whisper-soft elevation used only when a product image needs to breathe — exactly one drop-shadow in the entire system.
|
||||
- Tight two-row nav: slim `{component.global-nav}` + product-specific `{component.sub-nav-frosted}` with persistent right-aligned primary CTA.
|
||||
- Section rhythm across multiple pages: light hero → dark product tile → light utility tile → dark tile → parchment footer — a predictable pulse.
|
||||
|
||||
## Colors
|
||||
|
||||
> **Source pages analyzed:** homepage, environment, store, iPhone 17 Pro buy page, accessories index. The color system is identical across all five surfaces; only the surface-mode mix differs.
|
||||
|
||||
### Brand & Accent
|
||||
- **Action Blue** (`{colors.primary}` — #0066cc): The single brand-level interactive color. All text links, all blue pill CTAs ("Learn more", "Buy"), and the focus ring root. This is Apple's quiet but universal "click me" signal. Press state shifts to a slightly darker variant via the active scale transform rather than a hex change.
|
||||
- **Focus Blue** (`{colors.primary-focus}` — #0071e3): A marginally brighter sibling of Action Blue, reserved for the keyboard focus ring on buttons (`outline: 2px solid`).
|
||||
- **Sky Link Blue** (`{colors.primary-on-dark}` — #2997ff): A brighter blue used on dark surfaces for in-copy links and inline callouts, where Action Blue would disappear against the tile background.
|
||||
|
||||
### Surface
|
||||
- **Pure White** (`{colors.canvas}` — #ffffff): The dominant canvas. Content, utility cards, store tiles, configurator grids.
|
||||
- **Parchment** (`{colors.canvas-parchment}` — #f5f5f7): The signature Apple off-white. Used for alternating light tiles, footer region, and the default page canvas in store utility sections. Just different enough from white to create rhythm.
|
||||
- **Pearl Button** (`{colors.surface-pearl}` — #fafafc): A near-white used as the fill for secondary "ghost" buttons — lighter than the parchment canvas so the button still reads as a button against `{colors.canvas-parchment}`.
|
||||
- **Near-Black Tile 1** (`{colors.surface-tile-1}` — #272729): The primary dark-tile surface on the homepage product grid.
|
||||
- **Near-Black Tile 2** (`{colors.surface-tile-2}` — #2a2a2c): A micro-step lighter — used where a dark tile sits directly above or below Tile 1 to create the faintest separation.
|
||||
- **Near-Black Tile 3** (`{colors.surface-tile-3}` — #252527): A micro-step darker — used at the bottom of the stack and in embedded video/player frames.
|
||||
- **Pure Black** (`{colors.surface-black}` — #000000): Reserved for true void — video player backgrounds, edge-to-edge photographic overlays, the global nav bar background.
|
||||
- **Translucent Chip Gray** (`{colors.surface-chip-translucent}` — #d2d2d7): The base hex of the translucent gray chip used over photography for circular control buttons. In production, applied at ~64% alpha as `rgba(210, 210, 215, 0.64)`.
|
||||
|
||||
### Text
|
||||
- **Near-Black Ink** (`{colors.ink}` — #1d1d1f): The voice of every headline, every body paragraph, and the dark utility button's fill. Chosen instead of pure black to keep the page feeling photographic rather than printed.
|
||||
- **Body** (`{colors.body}` — #1d1d1f): Same hex as ink — Apple uses one near-black tone for all text on light surfaces.
|
||||
- **Body On Dark** (`{colors.body-on-dark}` — #ffffff): All text on dark tiles and on the global nav bar.
|
||||
- **Body Muted** (`{colors.body-muted}` — #cccccc): Secondary copy on dark tiles where pure white would be too loud.
|
||||
- **Ink Muted 80** (`{colors.ink-muted-80}` — #333333): Body text on the white Pearl Button surface — slightly softer than pure black.
|
||||
- **Ink Muted 48** (`{colors.ink-muted-48}` — #7a7a7a): Disabled button text and legal fine-print.
|
||||
|
||||
### Hairlines & Borders
|
||||
- **Divider Soft** (`{colors.divider-soft}` — #f0f0f0): The "border" tone on secondary buttons — functions as a ring shadow rather than a hard line. In production, often applied as `rgba(0, 0, 0, 0.04)`.
|
||||
- **Hairline** (`{colors.hairline}` — #e0e0e0): The 1px hairline border on store utility cards and configurator chips.
|
||||
|
||||
### Brand Gradient
|
||||
**No decorative gradients.** Atmospheric depth on product photography (the iPhone 17 Pro camera plate, the Apple Watch bands, AirPods reflections) is inherent to the imagery, not a CSS gradient overlay. The environment page's hero uses photographic atmosphere (mountain vista at dawn) but no gradient tokens are defined. Apple is the rare luxury-brand site with zero gradient-based design tokens.
|
||||
|
||||
## Typography
|
||||
|
||||
### Font Family
|
||||
- **Display**: `SF Pro Display, system-ui, -apple-system, sans-serif` — Apple's proprietary display face, optimized for sizes ≥ 19px. Defines the voice of every headline.
|
||||
- **Body / UI**: `SF Pro Text, system-ui, -apple-system, sans-serif` — the text-optimized variant used for body copy, captions, buttons, and links below 20px.
|
||||
- **OpenType features**: `font-variant-numeric: numerator` is enabled on numeric links (pricing tables, spec sheets). Display sizes rely on tight tracking rather than contextual ligatures.
|
||||
|
||||
### Hierarchy
|
||||
|
||||
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|
||||
|---|---|---|---|---|---|
|
||||
| `{typography.hero-display}` | 56px | 600 | 1.07 | -0.28px | Hero headline; the signature "Apple tight" tracking |
|
||||
| `{typography.display-lg}` | 40px | 600 | 1.10 | 0 | Tile headlines atop every product tile |
|
||||
| `{typography.display-md}` | 34px | 600 | 1.47 | -0.374px | Section heads (SF Pro Text at display proportions) |
|
||||
| `{typography.lead}` | 28px | 400 | 1.14 | 0.196px | Product tile subcopy |
|
||||
| `{typography.lead-airy}` | 24px | 300 | 1.5 | 0 | Environment-page lead paragraphs (the rare weight 300) |
|
||||
| `{typography.tagline}` | 21px | 600 | 1.19 | 0.231px | Sub-tile tagline; sub-nav category name |
|
||||
| `{typography.body-strong}` | 17px | 600 | 1.24 | -0.374px | Inline strong emphasis |
|
||||
| `{typography.body}` | 17px | 400 | 1.47 | -0.374px | Default paragraph |
|
||||
| `{typography.dense-link}` | 17px | 400 | 2.41 | 0 | Footer / store utility link lists (relaxed leading) |
|
||||
| `{typography.caption}` | 14px | 400 | 1.43 | -0.224px | Secondary captions, button text |
|
||||
| `{typography.caption-strong}` | 14px | 600 | 1.29 | -0.224px | Emphasized captions |
|
||||
| `{typography.button-large}` | 18px | 300 | 1.0 | 0 | Store hero CTAs (the rare weight 300) |
|
||||
| `{typography.button-utility}` | 14px | 400 | 1.29 | -0.224px | Utility/nav button labels |
|
||||
| `{typography.fine-print}` | 12px | 400 | 1.0 | -0.12px | Fine-print, footer body |
|
||||
| `{typography.micro-legal}` | 10px | 400 | 1.3 | -0.08px | Micro legal disclaimers |
|
||||
| `{typography.nav-link}` | 12px | 400 | 1.0 | -0.12px | Global nav menu items |
|
||||
|
||||
### Principles
|
||||
|
||||
- **Negative letter-spacing at display sizes.** Every headline at 17px and up carries a slight tracking tighten (`-0.12 → -0.374px`). This produces the iconic "Apple tight" headline cadence. Never used at 12px or below.
|
||||
- **Body copy at 17px, not 16px.** Apple breaks the SaaS convention and runs paragraph text at 17px. The extra pixel gives the page an unmistakable "reading, not scanning" pace.
|
||||
- **Weight 300 is real and rare.** Used deliberately on a handful of large-size reads (`{typography.button-large}` at 18px/300 and `{typography.lead-airy}` at 24px/300). It's not an accident — it's a light-atmosphere cue reserved for moments where the content should feel airy.
|
||||
- **Weight 600, not 700, for headlines.** Apple's headlines sit at weight 600. Weight 700 is used sparingly for `{typography.tagline}` (21px) when a touch more assertion is needed.
|
||||
- **Line-height is context-specific.** Display sizes use 1.07–1.19 (tight). Body uses 1.47. Utility link stacks in the footer/store use an unusually relaxed 2.41 (`{typography.dense-link}`). The 2.41 is not a bug — it's how the footer's dense link columns breathe.
|
||||
- **Weight 500 is deliberately absent.** The ladder is 300 / 400 / 600 / 700. Mid-weight readings always use 600.
|
||||
|
||||
### Note on Font Substitutes
|
||||
SF Pro is Apple's proprietary system font. When building off-system:
|
||||
|
||||
- Use `system-ui, -apple-system, BlinkMacSystemFont` as the first stack entry — on macOS/iOS/Safari this resolves to the real SF Pro.
|
||||
- For non-Apple platforms, **Inter** (Google Fonts, variable) is the closest open-source equivalent. Inter at weight 600 with `font-feature-settings: "ss03"` approximates SF Pro's rounded "a" character.
|
||||
- Nudge `letter-spacing` down by `-0.01em` on display sizes to re-create the Apple tight feel; Inter's default tracking runs slightly wider than SF Pro.
|
||||
- For body text, tighten line-height by `0.03` (from 1.47 → 1.44) when substituting Inter — Inter's taller x-height needs less leading.
|
||||
|
||||
## Layout
|
||||
|
||||
### Spacing System
|
||||
- **Base unit:** 8px. Sub-base values (2, 4, 5, 6, 7) are used for tight typographic adjustments; structural layout snaps to 8/12/16/20/24.
|
||||
- **Tokens:** `{spacing.xxs}` 4px · `{spacing.xs}` 8px · `{spacing.sm}` 12px · `{spacing.md}` 17px · `{spacing.lg}` 24px · `{spacing.xl}` 32px · `{spacing.xxl}` 48px · `{spacing.section}` 80px.
|
||||
- **Section vertical padding:** `{spacing.section}` (80px) inside a product tile; tiles stack edge-to-edge with 0 gap (the color change provides the break).
|
||||
- **Card padding:** `{spacing.lg}` (24px) inside utility grid cards.
|
||||
- **Button padding:** 8–11px vertical, 15–22px horizontal.
|
||||
- **Universal rhythm constants:** the 17px body line-height multiplier (~25px line) and 21px tagline size show up on every analyzed page.
|
||||
|
||||
### Grid & Container
|
||||
- **Max content width:** ~980px on text-heavy sections (environment), ~1440px on product grids (store, accessories), full-bleed for product tiles (homepage).
|
||||
- **Column patterns:** 3 to 5 column utility card grid on store/accessories; 2-column side-by-side tiles on homepage occasional sections; single-column centered stack on product tile heroes.
|
||||
- **Gutters:** 20–24px between cards in a utility grid.
|
||||
|
||||
### Whitespace Philosophy
|
||||
Apple's whitespace is the product's pedestal. Every tile begins with at least 64px of air above its headline and 48–64px below. Product renders are never crowded; the nearest content to a product image is at least 40px away. The footer is the only area that breaks this — there, Apple goes deliberately dense to make the full information architecture visible at a glance.
|
||||
|
||||
## Elevation & Depth
|
||||
|
||||
| Level | Treatment | Use |
|
||||
|---|---|---|
|
||||
| Flat | No shadow, no border | Full-bleed tiles, global nav, footer, body sections |
|
||||
| Soft hairline | 1px `rgba(0, 0, 0, 0.08)` border | Utility cards, sub-nav frosted-glass separator |
|
||||
| Backdrop blur | `backdrop-filter: blur(N)` on Parchment 80% | Sub-nav and the iPhone buy floating sticky bar |
|
||||
| Product shadow | `rgba(0, 0, 0, 0.22) 3px 5px 30px 0` | Product renders resting on a surface (the only true "shadow" in the system) |
|
||||
|
||||
**Shadow philosophy.** Apple uses **exactly one** drop-shadow, and it is applied to photographic product imagery — never to cards, never to buttons, never to text. Elevation in the UI comes from (a) surface-color change (light tile ↔ dark tile) and (b) backdrop-blur on sticky bars. The single shadow is about giving the product weight, not about UI hierarchy.
|
||||
|
||||
### Decorative Depth
|
||||
- **Atmospheric imagery** on the environment page (photographic vista) supplies mood; no CSS gradient involved.
|
||||
- **Edge-to-edge tile alternation** creates rhythm without borders or shadows — the color change itself is the divider.
|
||||
- **Backdrop-filter blur** on `{component.sub-nav-frosted}` and `{component.floating-sticky-bar}` creates a "floating over content" effect that's functional, not decorative.
|
||||
|
||||
## Shapes
|
||||
|
||||
### Border Radius Scale
|
||||
|
||||
| Token | Value | Use |
|
||||
|---|---|---|
|
||||
| `{rounded.none}` | 0px | Full-bleed product tiles (no corner rounding) |
|
||||
| `{rounded.xs}` | 5px | Inline links when styled as subtle chips (rare) |
|
||||
| `{rounded.sm}` | 8px | Dark utility buttons (Sign In, Bag), inline card imagery |
|
||||
| `{rounded.md}` | 11px | White Pearl Button capsules |
|
||||
| `{rounded.lg}` | 18px | Store utility cards, accessories grid cards |
|
||||
| `{rounded.pill}` | 9999px | Primary blue pill CTAs, sub-nav buy button, configurator option chips, search input — the signature Apple pill |
|
||||
| `{rounded.full}` | 9999px / 50% | Circular control chips floating over photography |
|
||||
|
||||
### Photography Geometry
|
||||
- **Hero imagery**: full-bleed, 21:9 or taller on the homepage; 16:9 on environment and shop pages. Product renders are photographic-realistic, often shot on a tinted surface that becomes the tile background.
|
||||
- **Product renders**: PNG/WebP with transparency; rest on a surface tile and pick up the system shadow.
|
||||
- **Accessory grid**: square 1:1 crops at `{rounded.lg}` (18px) radius, light neutral backgrounds, product centered with 20–40px internal padding.
|
||||
- **No rounded imagery in hero tiles** — images are full-bleed rectangular. Rounding (`{rounded.sm}`, `{rounded.lg}`) appears only on inline card imagery.
|
||||
- Lazy-loading via responsive `srcset` and `sizes` across all breakpoints; CDN-optimized WebP.
|
||||
|
||||
## Components
|
||||
|
||||
### Top Navigation
|
||||
|
||||
**`global-nav`** — Persistent, ultra-thin black nav bar pinned to the top of every page. Background `{colors.surface-black}`, height 44px, text `{colors.on-dark}` in `{typography.nav-link}` (12px / 400 / -0.12px tracking). Links are quiet, spaced ~20px apart, running edge-to-edge across the top. Right-aligned cluster: Search, Bag icons — always visible. On mobile, collapses to hamburger at ~834px and the Apple logo centers.
|
||||
|
||||
**`sub-nav-frosted`** — Surface-specific nav that sticks below the global nav. Background `{colors.canvas-parchment}` at 80% opacity with backdrop-filter blur, creating a frosted-glass effect. Height 52px. Content on left: product category name ("iPhone", "Store", "Accessories") in `{typography.tagline}` (21px / 600). Content right: inline nav links in `{typography.button-utility}` (14px), ending in a persistent `{component.button-primary}` ("Buy") or a utility link.
|
||||
|
||||
### Buttons
|
||||
|
||||
**`button-primary`** — The signature Apple action. Background `{colors.primary}` (Action Blue #0066cc), text `{colors.on-primary}` in `{typography.body}` (SF Pro Text 17px / 400), rounded `{rounded.pill}` (full pill — capsule-shaped), padding 11px × 22px. The full-pill radius IS the brand action signal.
|
||||
- Active state: `{component.button-primary-active}` — `transform: scale(0.95)` (the system-wide micro-interaction).
|
||||
- Focus state: `{component.button-primary-focus}` — 2px solid `{colors.primary-focus}` outline.
|
||||
|
||||
**`button-secondary-pill`** — Used as the second CTA when two blue pills appear together ("Learn more" / "Buy"). Background transparent, text `{colors.primary}`, 1px solid `{colors.primary}` border, rounded `{rounded.pill}`, padding 11px × 22px. Reads as a "ghost pill."
|
||||
|
||||
**`button-dark-utility`** — Global nav actions (Sign In, Bag, language selector). Background `{colors.ink}` (#1d1d1f), text `{colors.on-dark}` in `{typography.button-utility}` (14px / 400 / -0.224px tracking), rounded `{rounded.sm}` (8px), padding 8px × 15px. Active state shrinks via `transform: scale(0.95)`.
|
||||
|
||||
**`button-pearl-capsule`** — Product-card secondary button. Background `{colors.surface-pearl}` (#fafafc), text `{colors.ink-muted-80}` in `{typography.caption}` (14px), 3px solid `{colors.divider-soft}` border (functions as a soft ring rather than a visible line), rounded `{rounded.md}` (11px), padding 8px × 14px.
|
||||
|
||||
**`button-store-hero`** — A larger primary CTA used on store hero surfaces. Same Action Blue + Paper White as `{component.button-primary}`, but with `{typography.button-large}` (18px / 300 — note the rare weight 300) and slightly more padding (14px × 28px). Used sparingly on the store landing.
|
||||
|
||||
**`button-icon-circular`** — Floats over photography. 44 × 44px, background `{colors.surface-chip-translucent}` at ~64% alpha, icon in `{colors.ink}`, rounded `{rounded.full}`. Used for carousel controls, close buttons, and in-image controls (product image thumbnails on the iPhone buy page).
|
||||
|
||||
**`text-link`** — Inline body links in `{colors.primary}` (Action Blue). Underlined or non-underlined per context.
|
||||
|
||||
**`text-link-on-dark`** — Inline body links on dark tiles in `{colors.primary-on-dark}` (Sky Link Blue #2997ff) — Action Blue would disappear against `{colors.surface-tile-1}`.
|
||||
|
||||
### Cards & Containers
|
||||
|
||||
**`product-tile-light`** — Full-bleed light tile. Background `{colors.canvas}` (white), text `{colors.ink}`, rounded `{rounded.none}` (0 — tiles touch edges), vertical padding `{spacing.section}` (80px). Centered stack: product name in `{typography.display-lg}` (40px / 600) → one-line tagline in `{typography.lead}` (28px / 400) → two `{component.button-primary}` CTAs ("Learn more" / "Buy") → product render resting on the surface with the system shadow.
|
||||
|
||||
**`product-tile-parchment`** — Same as `{component.product-tile-light}` but on `{colors.canvas-parchment}` (#f5f5f7). Used to break two consecutive white tiles.
|
||||
|
||||
**`product-tile-dark`** — Full-bleed dark tile. Background `{colors.surface-tile-1}` (#272729), text `{colors.on-dark}`, rounded `{rounded.none}`, vertical padding `{spacing.section}` (80px). Same content stack as the light tile but with `{component.text-link-on-dark}` for inline copy and `{component.button-primary}` (Action Blue still works on the dark surface). Used on the homepage product grid as the alternating dark band.
|
||||
|
||||
**`product-tile-dark-2`** — Variant on `{colors.surface-tile-2}` (#2a2a2c). Used where a dark tile sits directly above or below `{component.product-tile-dark}` to create the faintest separation through micro-step lightness change.
|
||||
|
||||
**`product-tile-dark-3`** — Variant on `{colors.surface-tile-3}` (#252527). Used at the bottom of the stack and in embedded video/player frames.
|
||||
|
||||
**`store-utility-card`** — Used in store grid and accessories grid. Background `{colors.canvas}` (white), 1px solid `{colors.hairline}` border, rounded `{rounded.lg}` (18px), padding `{spacing.lg}` (24px). Top: product image (1:1 crop with `{rounded.sm}` (8px) inner image radius). Below: product name in `{typography.body-strong}` (17px / 600), price in `{typography.body}` (17px / 400), and a `{component.text-link}` ("Buy" or "Learn more"). No shadow by default; product render itself carries the system product-shadow.
|
||||
|
||||
**`configurator-option-chip`** — Pill-shaped tappable cell used in the iPhone 17 Pro buy page. Background `{colors.canvas}`, text `{colors.ink}` in `{typography.caption}`, rounded `{rounded.pill}`, padding 12px × 16px. Contains a small product thumbnail + label + price delta. Arranged in a grid of 4–5 options per row.
|
||||
|
||||
**`configurator-option-chip-selected`** — Selected state. Border upgrades to 2px solid `{colors.primary-focus}`. Same shape, same content.
|
||||
|
||||
**`environment-quote-card`** — A photographic-canvas hero specific to the environment page. Dark photographic backdrop (mountain vista at dawn) with `{colors.surface-tile-1}` as the fallback color, centered white-text headline in `{typography.display-lg}` (40px), small green "Apple 2030" pictographic logo above the headline, single `{component.button-primary}` below. Padding `{spacing.section}` (80px).
|
||||
|
||||
**`floating-sticky-bar`** — Floats at the bottom of the viewport on the iPhone 17 Pro buy page during scroll. Background `{colors.canvas-parchment}` at 80% opacity with `backdrop-filter: blur(N)`, height 64px, padding 12px × 32px. Left: running price total in `{typography.body}`. Right: `{component.button-primary}` ("Add to Bag").
|
||||
|
||||
### Inputs & Forms
|
||||
|
||||
**`search-input`** — The accessories search input. Background `{colors.canvas}`, text `{colors.ink}` in `{typography.body}` (17px), 1px solid `rgba(0, 0, 0, 0.08)` border, rounded `{rounded.pill}` (full pill — search is also pill-shaped, matching the CTA grammar), padding 12px × 20px, height 44px. Leading icon: search glyph at 14px, muted tint.
|
||||
|
||||
Error and validation states were not surfaced in the analyzed pages.
|
||||
|
||||
### Footer
|
||||
|
||||
**`footer`** — Background `{colors.canvas-parchment}` (#f5f5f7), text `{colors.ink-muted-80}`. Link columns in `{typography.dense-link}` (17px / 400 / 2.41 line-height — the relaxed leading is what makes the dense columns scannable). Column headings in `{typography.caption-strong}` (14px / 600). Legal row at the very bottom in `{typography.fine-print}` (12px / 400) with `{colors.ink-muted-48}` text. Vertical padding 64px.
|
||||
|
||||
## Do's and Don'ts
|
||||
|
||||
### Do
|
||||
- Use `{colors.primary}` (Action Blue #0066cc) for every interactive element — links, pill CTAs, focus signals — and nothing else. The single accent is non-negotiable.
|
||||
- Set headlines in `{typography.hero-display}` or `{typography.display-lg}` with negative letter-spacing (`-0.28 → -0.374px`) to get the signature "Apple tight" cadence.
|
||||
- Run body copy at `{typography.body}` (17px / 400 / 1.47 / -0.374px) — not 16px. The extra pixel defines the brand's reading pace.
|
||||
- Alternate `{component.product-tile-light}` (or parchment) and `{component.product-tile-dark}` for full-bleed section rhythm. The color change IS the divider.
|
||||
- Reserve `{rounded.pill}` for the primary blue CTA and any other element that should read as an "action" (configurator chips, search input, sticky bar CTA).
|
||||
- Apply the single product-shadow (`rgba(0, 0, 0, 0.22) 3px 5px 30px`) only to product renders resting on a surface — never on cards, buttons, or text.
|
||||
- Use `transform: scale(0.95)` as the active/press state on every button — it's the system-wide micro-interaction.
|
||||
- Keep the global nav `{colors.surface-black}` (true black) — it's the only place pure black appears on most pages.
|
||||
|
||||
### Don't
|
||||
- Don't introduce a second accent color; every "click me" signal is `{colors.primary}` (Action Blue).
|
||||
- Don't add shadows to cards, buttons, or text — shadow is reserved for product imagery.
|
||||
- Don't use gradients as decorative backgrounds; atmosphere comes from photography.
|
||||
- Don't set body copy at weight 500 — Apple's ladder is 300 / 400 / 600 / 700, with 500 deliberately absent. Body is always 400; strong inline is 600; display is 600.
|
||||
- Don't round full-bleed tiles — tiles are rectangular and edge-to-edge; the color change is the divider.
|
||||
- Don't tighten line-height below 1.47 for body copy — the editorial leading is part of the brand.
|
||||
- Don't mix radii grammars — use `{rounded.sm}` for compact utility, `{rounded.lg}` for utility cards, `{rounded.pill}` for pills, and nothing in between (except the rare `{rounded.md}` Pearl Button).
|
||||
- Don't use `{colors.primary-on-dark}` (Sky Link Blue) on light surfaces — it's the dark-tile-only variant. Action Blue is for light surfaces.
|
||||
|
||||
## Responsive Behavior
|
||||
|
||||
### Breakpoints
|
||||
|
||||
| Name | Width | Key Changes |
|
||||
|---|---|---|
|
||||
| Small phone | ≤ 419px | Single-column tiles; sub-nav collapses to category name + primary CTA only; hero typography drops to 28px |
|
||||
| Phone | 420–640px | Single-column stack; product renders scale to 80% of tile width; hero h1 drops to 34px |
|
||||
| Large phone | 641–735px | Tiles transition to tighter padding (48px vertical vs 80px); fine-print wraps |
|
||||
| Tablet portrait | 736–833px | Global nav collapses to hamburger; sub-nav hides category chips, keeps primary CTA |
|
||||
| Tablet landscape | 834–1023px | Global nav returns fully expanded; 3-column utility grids become 2-column |
|
||||
| Small desktop | 1024–1068px | Product tiles use 2/3 width with margin gutters; hero h1 stays at 40px |
|
||||
| Desktop | 1069–1440px | Full layout; 4–5 column store grids; 1440px content max |
|
||||
| Wide desktop | ≥ 1441px | Content locks at 1440px, margins absorb extra width |
|
||||
|
||||
The structural breakpoints that matter for agents: 1440px (content lock), 1068px (small-desktop), 833px (tablet landscape switch), 734px (tablet portrait), 640px (phone), 480px (small phone).
|
||||
|
||||
### Touch Targets
|
||||
- Minimum 44 × 44px. `{component.button-primary}` lands at ~44 × 100px (with the full-pill radius making the visible hit area more generous than the label suggests).
|
||||
- `{component.button-icon-circular}` is exactly 44 × 44px.
|
||||
- Global nav utility links are smaller (~32 × 80px) — they deliberately sit at a tighter target because they're precision desktop actions, and the mobile hamburger replaces them at ≤ 833px.
|
||||
|
||||
### Collapsing Strategy
|
||||
- **Global nav**: full horizontal link row on desktop → collapses to Apple logo + hamburger + bag icon at 834px and below.
|
||||
- **Sub-nav**: category name + inline links + primary CTA → category name + primary CTA only at mobile; inline links move into a hamburger tray.
|
||||
- **Product tiles**: stack from 2-column to 1-column at 834px; vertical padding tightens from 80px → 48px at small-phone.
|
||||
- **Utility grids** (store, accessories): 5-col → 4-col (1440px) → 3-col (1068px) → 2-col (834px) → 1-col (640px).
|
||||
- **Hero typography**: `{typography.hero-display}` (56px) → `{typography.display-lg}` (40px) at 1068px → 34px at 640px → 28px at 419px.
|
||||
|
||||
### Image Behavior
|
||||
- All product imagery uses responsive `srcset` with breakpoint-matched crops.
|
||||
- Hero photography may switch art direction at mobile (e.g., the environment page's vista crops to a taller aspect ratio on mobile, framing the subject differently).
|
||||
- Product renders maintain their 1:1 or 4:3 aspect ratios across breakpoints; only scale changes.
|
||||
- Lazy-loading is default; the above-fold hero loads eagerly.
|
||||
|
||||
## Iteration Guide
|
||||
|
||||
1. Focus on ONE component at a time. Reference its YAML key directly (`{component.product-tile-dark}`, `{component.search-input}`).
|
||||
2. Variants of an existing component (`-active`, `-focus`, `-2`, `-3`) live as separate entries in `components:`.
|
||||
3. Use `{token.refs}` everywhere — never inline hex.
|
||||
4. Never document hover. Default and Active/Pressed states only.
|
||||
5. Display headlines stay SF Pro Display 600 with negative letter-spacing. Body stays SF Pro Text 400 at 17px. The boundary is unbreakable.
|
||||
6. The single drop-shadow (`rgba(0, 0, 0, 0.22) 3px 5px 30px`) is reserved for product photography only.
|
||||
7. When in doubt about emphasis: alternate surface (light → dark tile) before adding chrome.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- Form validation and error states were not surfaced on the analyzed pages; only the neutral search input is documented.
|
||||
- The homepage's embedded video/player frame uses `{colors.surface-black}`; interior player controls are not documented (they're a platform widget, not a web-design token).
|
||||
- Some component imagery is dynamic (rotating product hero) and its specific copy varies per surface — component specs name the structure, not the rotating content.
|
||||
- Dark-mode counterparts for store and accessories utility cards were not surfaced on the analyzed pages; the system documented is the daytime/light-dominant variant Apple ships by default.
|
||||
- Atmospheric photography (environment page mountain vista) is a content asset, not a design token; the documented `{component.environment-quote-card}` describes the structural surface only.
|
||||
- The exact backdrop-filter blur radius on `{component.sub-nav-frosted}` and `{component.floating-sticky-bar}` is platform-dependent; production CSS uses `saturate(180%) blur(20px)` as a typical baseline but the value isn't formalized as a token.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Apple Inspired Design System
|
||||
|
||||
Design system details have been moved to: https://getdesign.md/apple/design-md
|
||||
|
||||
You can also view previews, dark mode examples, and download options on getdesign.md.
|
||||
@@ -0,0 +1,749 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Apple — reference components</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Reference fixture for design-systems/apple. Every visible
|
||||
token comes from tokens.css; the page itself follows Apple's
|
||||
editorial rules — pale-gray feature ladder over white, single
|
||||
Apple Action Blue accent capped at ≤2 visible uses per screen,
|
||||
SF Pro display at compressed tracking, and the signature 980px
|
||||
capsule CTA."
|
||||
/>
|
||||
|
||||
<style>
|
||||
/* :root paste — sourced verbatim from
|
||||
design-systems/apple/tokens.css. Keep this block in sync
|
||||
when tokens.css changes. The agent prompt instructs the
|
||||
Designer panelist to paste this block as the FIRST thing in
|
||||
their <style>, then reference everything below via var(...). */
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--surface: #f5f5f7;
|
||||
--surface-warm: #fbfbfd;
|
||||
|
||||
--fg: #1d1d1f;
|
||||
--fg-2: #424245;
|
||||
--muted: #6e6e73;
|
||||
--meta: #86868b;
|
||||
|
||||
--border: #d2d2d7;
|
||||
--border-soft: #e8e8ed;
|
||||
|
||||
--accent: #0071e3;
|
||||
--accent-on: #ffffff;
|
||||
--accent-hover: #0077ed;
|
||||
--accent-active: #0066cc;
|
||||
|
||||
--success: #16a34a;
|
||||
--warn: #eab308;
|
||||
--danger: #dc2626;
|
||||
|
||||
--font-display:
|
||||
"SF Pro Display", "SF Pro Icons", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--font-body:
|
||||
"SF Pro Text", "SF Pro Icons", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--font-mono:
|
||||
"SF Mono", ui-monospace, "JetBrains Mono", Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
--text-xs: 12px;
|
||||
--text-sm: 14px;
|
||||
--text-base: 17px;
|
||||
--text-lg: 21px;
|
||||
--text-xl: 28px;
|
||||
--text-2xl: 40px;
|
||||
--text-3xl: 56px;
|
||||
--text-4xl: 80px;
|
||||
|
||||
--leading-body: 1.47;
|
||||
--leading-tight: 1.05;
|
||||
|
||||
--tracking-display: -0.015em;
|
||||
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-12: 48px;
|
||||
|
||||
--section-y-desktop: 100px;
|
||||
--section-y-tablet: 64px;
|
||||
--section-y-phone: 40px;
|
||||
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 18px;
|
||||
--radius-pill: 980px;
|
||||
|
||||
--elev-flat: none;
|
||||
--elev-ring: 0 0 0 1px var(--border);
|
||||
--elev-raised: 0 12px 32px rgba(0, 0, 0, 0.08);
|
||||
|
||||
--focus-ring: 0 0 0 4px color-mix(in oklab, var(--accent), transparent 65%);
|
||||
|
||||
--motion-fast: 150ms;
|
||||
--motion-base: 220ms;
|
||||
--ease-standard: cubic-bezier(0.28, 0, 0.22, 1);
|
||||
|
||||
--container-max: 1024px;
|
||||
--container-gutter-desktop: 22px;
|
||||
--container-gutter-tablet: 18px;
|
||||
--container-gutter-phone: 16px;
|
||||
}
|
||||
|
||||
/* ─── Reset (minimal) ───────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-base);
|
||||
line-height: var(--leading-body);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.022em; /* SF Pro Text body, per DESIGN.md §3 */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ─── Layout primitives ─────────────────────────────────────── */
|
||||
.container {
|
||||
max-width: var(--container-max);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--container-gutter-desktop);
|
||||
}
|
||||
section {
|
||||
padding-block: var(--section-y-desktop);
|
||||
}
|
||||
/* Apple uses surface contrast, not borders, to separate chapters
|
||||
(DESIGN.md §5 — "section transitions rely more on surface
|
||||
changes than decorative separators"). The .band-* helpers
|
||||
below switch the chapter canvas while the container stays
|
||||
constant. */
|
||||
.band-light { background: var(--bg); color: var(--fg); }
|
||||
.band-soft { background: var(--surface); color: var(--fg); }
|
||||
.band-warm { background: var(--surface-warm); color: var(--fg); }
|
||||
@media (max-width: 1023px) {
|
||||
.container { padding-inline: var(--container-gutter-tablet); }
|
||||
section { padding-block: var(--section-y-tablet); }
|
||||
}
|
||||
@media (max-width: 639px) {
|
||||
.container { padding-inline: var(--container-gutter-phone); }
|
||||
section { padding-block: var(--section-y-phone); }
|
||||
}
|
||||
|
||||
/* ─── Typography ────────────────────────────────────────────── */
|
||||
h1, h2, h3 {
|
||||
font-family: var(--font-display);
|
||||
margin: 0;
|
||||
color: var(--fg);
|
||||
}
|
||||
/* DESIGN.md §3 — Hero Display L (56px / 600 / 1.07). We set
|
||||
tracking via the display token so all hero copy compresses
|
||||
consistently. */
|
||||
h1 {
|
||||
font-size: var(--text-3xl);
|
||||
font-weight: 600;
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: var(--tracking-display);
|
||||
}
|
||||
h2 {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
letter-spacing: var(--tracking-display);
|
||||
}
|
||||
h3 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 600;
|
||||
line-height: 1.14;
|
||||
letter-spacing: -0.007em; /* DESIGN.md §3 — Card/Product Title */
|
||||
}
|
||||
p { margin: 0; }
|
||||
|
||||
.lead {
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.38;
|
||||
color: var(--fg-2);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.011em;
|
||||
}
|
||||
.body-muted { color: var(--muted); }
|
||||
.body-meta { color: var(--meta); font-size: var(--text-sm); }
|
||||
.body-sm { font-size: var(--text-sm); }
|
||||
|
||||
/* `.eyebrow` — small uppercase label above hero / section heads.
|
||||
DESIGN.md §3 has no eyebrow row in the hierarchy table; this
|
||||
is the agreed pattern for OD reference fixtures and uses the
|
||||
12px micro-UI tier with positive tracking so the rule reads
|
||||
as deliberate. */
|
||||
.eyebrow {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.stack-3 > * + * { margin-block-start: var(--space-3); }
|
||||
.stack-4 > * + * { margin-block-start: var(--space-4); }
|
||||
.stack-6 > * + * { margin-block-start: var(--space-6); }
|
||||
|
||||
/* ─── Buttons ─────────────────────────────────────────────────
|
||||
* Apple's CTA family is split across two silhouettes:
|
||||
* - .btn-pill / .btn-pill-secondary — the signature 980px
|
||||
* capsule used on every marketing hero and product page.
|
||||
* - .btn / .btn-primary / .btn-secondary — the 8/12px-radius
|
||||
* compact controls used in commerce / configurator
|
||||
* surfaces where the capsule would feel too marketing-y.
|
||||
*
|
||||
* Both share base sizing (8/15px padding per DESIGN.md §4) and
|
||||
* the focus halo. Hover lifts the blue (--accent-hover) rather
|
||||
* than darkens it — Apple's documented behaviour. Press scales
|
||||
* the button down 2% per DESIGN.md §4 ("active controls
|
||||
* commonly reduce scale to indicate physical press"). */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 8px 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--font-body);
|
||||
font-weight: 500;
|
||||
font-size: var(--text-sm);
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background-color var(--motion-fast) var(--ease-standard),
|
||||
border-color var(--motion-fast) var(--ease-standard),
|
||||
transform var(--motion-fast) var(--ease-standard),
|
||||
box-shadow var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
.btn:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
.btn:active { transform: scale(0.98); }
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: var(--accent-on);
|
||||
}
|
||||
.btn-primary:hover { background: var(--accent-hover); }
|
||||
.btn-primary:active { background: var(--accent-active); }
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--fg);
|
||||
border-color: var(--border);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: var(--surface);
|
||||
border-color: var(--meta);
|
||||
}
|
||||
|
||||
/* The Apple capsule. --radius-pill is bound to 980px in tokens
|
||||
(the literal published Apple value); the geometry is the
|
||||
brand signature and earns its own component. */
|
||||
.btn-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 12px 22px;
|
||||
border: none;
|
||||
border-radius: var(--radius-pill);
|
||||
font-family: var(--font-body);
|
||||
font-weight: 400;
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.005em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
background: var(--accent);
|
||||
color: var(--accent-on);
|
||||
transition:
|
||||
background-color var(--motion-fast) var(--ease-standard),
|
||||
transform var(--motion-fast) var(--ease-standard),
|
||||
box-shadow var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
.btn-pill:hover { background: var(--accent-hover); }
|
||||
.btn-pill:active { background: var(--accent-active); transform: scale(0.98); }
|
||||
.btn-pill:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
.btn-pill-secondary {
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
box-shadow: var(--elev-ring);
|
||||
}
|
||||
.btn-pill-secondary:hover {
|
||||
background: var(--surface);
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
.btn-pill-secondary:active {
|
||||
color: var(--accent-active);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* ─── Inputs ────────────────────────────────────────────────
|
||||
* Apple's retail inputs are border-led rather than fill-led —
|
||||
* a hairline #d2d2d7 box on white, dark text, the focus halo
|
||||
* tightens the border to --accent and adds the blue glow.
|
||||
* Density stays quiet so device imagery dominates. */
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.field label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--fg);
|
||||
letter-spacing: -0.016em;
|
||||
}
|
||||
.field input {
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.2;
|
||||
outline: none;
|
||||
transition:
|
||||
border-color var(--motion-fast) var(--ease-standard),
|
||||
box-shadow var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
.field input::placeholder { color: var(--muted); }
|
||||
.field input:hover { border-color: var(--meta); }
|
||||
.field input:focus-visible {
|
||||
border-color: var(--accent);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
.field-help {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ─── Cards ─────────────────────────────────────────────────
|
||||
* Two intents:
|
||||
* - .card plain editorial / product card. Lives on
|
||||
* pale gray (--surface) bands; lifts to
|
||||
* white (--bg) with a hairline. No shadow
|
||||
* by default — surface contrast does the
|
||||
* work.
|
||||
* - .card-feature featured spotlight. Squircle 18px
|
||||
* (--radius-lg), the soft Apple shadow on
|
||||
* hover. Used sparingly — one per band. */
|
||||
.card {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
transition: box-shadow var(--motion-base) var(--ease-standard);
|
||||
}
|
||||
.card:hover {
|
||||
box-shadow: var(--elev-raised);
|
||||
}
|
||||
.card-feature {
|
||||
background: var(--surface-warm);
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-8) var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
box-shadow: var(--elev-raised);
|
||||
}
|
||||
|
||||
/* ─── Badges ──────────────────────────────────────────────── */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: 4px var(--space-3);
|
||||
border-radius: var(--radius-pill);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.badge-success {
|
||||
color: var(--success);
|
||||
background: color-mix(in oklab, var(--success), transparent 90%);
|
||||
}
|
||||
.badge-muted {
|
||||
color: var(--muted);
|
||||
background: color-mix(in oklab, var(--muted), transparent 88%);
|
||||
}
|
||||
.badge-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* ─── Links ─────────────────────────────────────────────────
|
||||
* Apple's link tone uses Body Link Blue (#0066cc, our
|
||||
* --accent-active) for inline reading, with the chevron-arrow
|
||||
* suffix on call-to-action links. The base color is --accent
|
||||
* for buttons / nav; here we re-bind for the inline reading
|
||||
* tone via --accent-active. */
|
||||
a {
|
||||
color: var(--accent-active);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
/* ─── Kbd ─────────────────────────────────────────────────── */
|
||||
kbd {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 500;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
color: var(--fg-2);
|
||||
}
|
||||
|
||||
/* ─── Distinctive: glass nav strip ──────────────────────────
|
||||
* DESIGN.md §4 — "Global Marketing Nav: compact dark translucent
|
||||
* bar with small-type links and restrained iconography." We
|
||||
* implement the light counterpart: a frosted white strip with
|
||||
* backdrop-blur, hairline bottom border, and SF Pro Text 14px
|
||||
* link tier. The blur is what gives Apple chrome its signature
|
||||
* "atmospheric layering" (DESIGN.md §6). */
|
||||
.nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: color-mix(in oklab, var(--bg), transparent 20%);
|
||||
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
||||
backdrop-filter: saturate(180%) blur(20px);
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: var(--container-max);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--container-gutter-desktop);
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-6);
|
||||
}
|
||||
.nav-brand {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-base);
|
||||
font-weight: 500;
|
||||
color: var(--fg);
|
||||
letter-spacing: -0.011em;
|
||||
}
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: var(--space-6);
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.nav-links a {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 400;
|
||||
color: var(--fg);
|
||||
letter-spacing: -0.016em;
|
||||
opacity: 0.8;
|
||||
transition: opacity var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
.nav-links a:hover {
|
||||
opacity: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (max-width: 639px) {
|
||||
.nav-inner { gap: var(--space-3); padding-inline: var(--container-gutter-phone); }
|
||||
.nav-links { gap: var(--space-4); }
|
||||
}
|
||||
|
||||
/* ─── Section-specific layout ───────────────────────────── */
|
||||
.hero-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: var(--space-12);
|
||||
align-items: end;
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
.hero-grid { grid-template-columns: 1fr; gap: var(--space-8); }
|
||||
}
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
margin-block-start: var(--space-6);
|
||||
}
|
||||
.hero-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-5);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg);
|
||||
}
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr 1fr;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
.features-grid { grid-template-columns: 1fr 1fr; }
|
||||
.features-grid > :first-child { grid-column: span 2; }
|
||||
}
|
||||
@media (max-width: 639px) {
|
||||
.features-grid { grid-template-columns: 1fr; }
|
||||
.features-grid > :first-child { grid-column: auto; }
|
||||
}
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: var(--space-12);
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
.form-row { grid-template-columns: 1fr; gap: var(--space-8); }
|
||||
}
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
max-width: 420px;
|
||||
}
|
||||
.form-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-3);
|
||||
margin-block-start: var(--space-2);
|
||||
}
|
||||
.icon { width: 16px; height: 16px; flex-shrink: 0; }
|
||||
.row-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
NAV — Apple's distinctive frosted-glass strip. Sticky to top,
|
||||
44px tall, SF Pro Text 14px links at 0.8 opacity that
|
||||
brighten on hover. Backdrop saturate + blur is what gives
|
||||
Apple chrome its atmospheric layering (DESIGN.md §6).
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<header class="nav" data-od-id="nav">
|
||||
<div class="nav-inner">
|
||||
<span class="nav-brand">Apple</span>
|
||||
<nav aria-label="Primary">
|
||||
<ul class="nav-links">
|
||||
<li><a href="#hero">Tokens</a></li>
|
||||
<li><a href="#features">System</a></li>
|
||||
<li><a href="#form">Contact</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
HERO — exercises: h1 (56px display, tracking -0.015em),
|
||||
.lead, .eyebrow, .btn-pill (the signature 980px capsule)
|
||||
paired with .btn-pill-secondary (ring-edge counterpart),
|
||||
kbd, .badge-success. Asymmetric grid (1.3:1) so the
|
||||
composition doesn't read as a generic centered template.
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<section class="band-light" data-od-id="hero" id="hero">
|
||||
<div class="container">
|
||||
<div class="hero-grid">
|
||||
<div class="stack-4">
|
||||
<p class="eyebrow">Reference fixture · Apple</p>
|
||||
<h1>Quiet chrome. Loud product. One token block.</h1>
|
||||
<p class="lead" style="max-width: 52ch">
|
||||
The interface engineered to disappear, distilled into
|
||||
fifty-six tokens. Pale-gray feature ladder over white,
|
||||
a single Apple Action Blue, SF Pro at compressed
|
||||
tracking, and the 980-pixel capsule that has carried
|
||||
every Buy button for a decade.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a href="./tokens.css" class="btn-pill">
|
||||
View tokens
|
||||
<svg
|
||||
class="icon"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="./DESIGN.md" class="btn-pill btn-pill-secondary">
|
||||
Read the spec
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="hero-meta" aria-label="System status">
|
||||
<div class="row-between">
|
||||
<span class="body-sm">System status</span>
|
||||
<span class="badge badge-success">
|
||||
<span class="badge-dot" aria-hidden="true"></span>
|
||||
Stable
|
||||
</span>
|
||||
</div>
|
||||
<p class="body-sm body-muted">
|
||||
Last reviewed
|
||||
<time datetime="2026-05-14">2026-05-14</time> · v0.1
|
||||
</p>
|
||||
<p class="body-sm body-muted">
|
||||
Press <kbd>⌘</kbd> <kbd>K</kbd> to search tokens.
|
||||
</p>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
FEATURES — band switches to pale Apple gray (--surface),
|
||||
cards lift back to white with a hairline. The first card
|
||||
is .card-feature (squircle, soft Apple shadow), the next
|
||||
two are plain .card. Each card describes a real property
|
||||
of this fixture (no "feature one/two/three" filler).
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<section class="band-soft" data-od-id="features" id="features">
|
||||
<div class="container">
|
||||
<div class="stack-3" style="max-width: 720px">
|
||||
<p class="eyebrow">What this fixture exercises</p>
|
||||
<h2>Every component below uses only var(--*).</h2>
|
||||
<p class="lead body-muted">
|
||||
Three cards, three real schema decisions worth
|
||||
inspecting. Rebind the token block and the same shapes
|
||||
follow.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="features-grid" style="margin-block-start: var(--space-12)">
|
||||
<article class="card-feature">
|
||||
<p class="eyebrow">Featured</p>
|
||||
<h3>Surface ladder, three stops, no aliasing.</h3>
|
||||
<p class="body-muted">
|
||||
Pure white retail canvas, near-white bridge, pale
|
||||
Apple gray feature band. Apple's three light tiers
|
||||
are real, so we keep --surface-warm independent of
|
||||
--surface instead of collapsing it. Cards on the
|
||||
gray band lift back to white with a hairline.
|
||||
</p>
|
||||
<div>
|
||||
<a href="./tokens.css" class="btn btn-secondary">
|
||||
Inspect tokens
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>Accent discipline</h3>
|
||||
<p class="body-muted body-sm">
|
||||
--accent appears at most twice on this screen — the
|
||||
primary capsule CTA and the focus halo. The hero
|
||||
status uses --success instead, so the page does not
|
||||
feel mono-blue.
|
||||
</p>
|
||||
<a href="./DESIGN.md" class="body-sm">Read the rule →</a>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>Type at scale</h3>
|
||||
<p class="body-muted body-sm">
|
||||
SF Pro Display for the hero, SF Pro Text for body and
|
||||
controls, body baseline at 17px (not 16), display
|
||||
tracking compressed to -0.015em. Hierarchy comes from
|
||||
size and weight — never a third type face.
|
||||
</p>
|
||||
<a href="./tokens.css" class="body-sm">Inspect typography →</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════
|
||||
FORM — exercises: .field, input :focus-visible (border
|
||||
tightens to --accent + the 4px blue halo), .btn-primary
|
||||
(the compact 8px-radius commerce CTA), .btn-secondary
|
||||
(border-led counterpart), .field-help. Band switches to
|
||||
the warm near-white tier so the form reads as a quiet
|
||||
transactional surface, not a hero.
|
||||
═══════════════════════════════════════════════════════════════ -->
|
||||
<section class="band-warm" data-od-id="form" id="form">
|
||||
<div class="container">
|
||||
<div class="form-row">
|
||||
<div class="stack-4">
|
||||
<p class="eyebrow">Form components</p>
|
||||
<h2>Inputs inherit the same tokens.</h2>
|
||||
<p class="lead body-muted" style="max-width: 48ch">
|
||||
Border-led containment, dark Near-Black Ink text, the
|
||||
focus halo at four pixels of soft Apple Action Blue.
|
||||
The submit button reuses the compact .btn-primary —
|
||||
no new token introduced for this section.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="form" onsubmit="event.preventDefault();">
|
||||
<div class="field">
|
||||
<label for="email">Apple ID email</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@icloud.com"
|
||||
autocomplete="email"
|
||||
required
|
||||
/>
|
||||
<p class="field-help">
|
||||
We'll send the spec PDF and nothing else.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Send the spec
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary">
|
||||
Skip for now
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Loaded 100 of 1089 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user