Compare commits
19 Commits
v0.1.6
...
286b46c6e6
Author | SHA1 | Date | |
---|---|---|---|
286b46c6e6
|
|||
b780d27ace
|
|||
831fcb7865
|
|||
3f8bbff7c2
|
|||
5cb73a9ed3
|
|||
6ce8d44c4b
|
|||
167cfb35f7
|
|||
c8377ceabf
|
|||
e917c563f2
|
|||
a76933c23b
|
|||
8d960b7a40
|
|||
d3967c782f
|
|||
63b6fd5142
|
|||
27935324c7
|
|||
594d7e7aef
|
|||
be86c11e74
|
|||
619675b551
|
|||
d4d98e2e91
|
|||
8dac463345
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,7 +5,6 @@
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
cloudflare-config.yml
|
||||
my-blog/public/
|
||||
dist
|
||||
node_modules
|
||||
@@ -13,3 +12,7 @@ package-lock.json
|
||||
my-blog/static/assets/comment-atproto-*
|
||||
bin/ailog
|
||||
docs
|
||||
my-blog/static/index.html
|
||||
my-blog/templates/oauth-assets.html
|
||||
cloudflared-config.yml
|
||||
.config
|
||||
|
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ailog"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
edition = "2021"
|
||||
authors = ["syui"]
|
||||
description = "A static blog generator with AI features"
|
||||
|
32
Dockerfile
32
Dockerfile
@@ -1,32 +0,0 @@
|
||||
# Multi-stage build for ailog
|
||||
FROM rust:1.75 as builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the binary
|
||||
COPY --from=builder /usr/src/app/target/release/ailog /usr/local/bin/ailog
|
||||
|
||||
# Copy blog content
|
||||
COPY my-blog ./blog
|
||||
|
||||
# Build static site
|
||||
RUN ailog build blog
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Run server
|
||||
CMD ["ailog", "serve", "blog"]
|
128
action.yml
128
action.yml
@@ -1,128 +0,0 @@
|
||||
name: 'ailog Static Site Generator'
|
||||
description: 'AI-powered static blog generator with atproto integration'
|
||||
author: 'syui'
|
||||
|
||||
branding:
|
||||
icon: 'book-open'
|
||||
color: 'orange'
|
||||
|
||||
inputs:
|
||||
content-dir:
|
||||
description: 'Content directory containing markdown files'
|
||||
required: false
|
||||
default: 'content'
|
||||
output-dir:
|
||||
description: 'Output directory for generated site'
|
||||
required: false
|
||||
default: 'public'
|
||||
template-dir:
|
||||
description: 'Template directory'
|
||||
required: false
|
||||
default: 'templates'
|
||||
static-dir:
|
||||
description: 'Static assets directory'
|
||||
required: false
|
||||
default: 'static'
|
||||
config-file:
|
||||
description: 'Configuration file path'
|
||||
required: false
|
||||
default: 'ailog.toml'
|
||||
ai-integration:
|
||||
description: 'Enable AI features'
|
||||
required: false
|
||||
default: 'true'
|
||||
atproto-integration:
|
||||
description: 'Enable atproto/OAuth features'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
outputs:
|
||||
site-url:
|
||||
description: 'Generated site URL'
|
||||
value: ${{ steps.generate.outputs.site-url }}
|
||||
build-time:
|
||||
description: 'Build time in seconds'
|
||||
value: ${{ steps.generate.outputs.build-time }}
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Cache ailog binary
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ./bin
|
||||
key: ailog-bin-${{ runner.os }}
|
||||
restore-keys: |
|
||||
ailog-bin-${{ runner.os }}
|
||||
|
||||
- name: Setup ailog binary
|
||||
shell: bash
|
||||
run: |
|
||||
# Check if pre-built binary exists
|
||||
if [ -f "./bin/ailog-linux-x86_64" ]; then
|
||||
echo "Using pre-built binary from repository"
|
||||
chmod +x ./bin/ailog-linux-x86_64
|
||||
CURRENT_VERSION=$(./bin/ailog-linux-x86_64 --version 2>/dev/null || echo "unknown")
|
||||
echo "Binary version: $CURRENT_VERSION"
|
||||
else
|
||||
echo "No pre-built binary found, trying to build from source..."
|
||||
if command -v cargo >/dev/null 2>&1; then
|
||||
cargo build --release
|
||||
mkdir -p ./bin
|
||||
cp ./target/release/ailog ./bin/ailog-linux-x86_64
|
||||
echo "Built from source: $(./bin/ailog-linux-x86_64 --version 2>/dev/null)"
|
||||
else
|
||||
echo "Error: No binary found and cargo not available"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Setup Node.js for OAuth app
|
||||
if: ${{ inputs.atproto-integration == 'true' }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Build OAuth app
|
||||
if: ${{ inputs.atproto-integration == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
if [ -d "oauth" ]; then
|
||||
cd oauth
|
||||
npm install
|
||||
npm run build
|
||||
cp -r dist/* ../${{ inputs.static-dir }}/
|
||||
fi
|
||||
|
||||
- name: Generate site
|
||||
id: generate
|
||||
shell: bash
|
||||
run: |
|
||||
start_time=$(date +%s)
|
||||
|
||||
# Change to blog directory and run build
|
||||
# Note: ailog build only takes a path argument, not options
|
||||
if [ -d "my-blog" ]; then
|
||||
cd my-blog
|
||||
../bin/ailog-linux-x86_64 build
|
||||
else
|
||||
# If no my-blog directory, use current directory
|
||||
./bin/ailog-linux-x86_64 build .
|
||||
fi
|
||||
|
||||
end_time=$(date +%s)
|
||||
build_time=$((end_time - start_time))
|
||||
|
||||
echo "build-time=${build_time}" >> $GITHUB_OUTPUT
|
||||
echo "site-url=file://$(pwd)/${{ inputs.output-dir }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Display build summary
|
||||
shell: bash
|
||||
run: |
|
||||
echo "✅ ailog build completed successfully"
|
||||
echo "📁 Output directory: ${{ inputs.output-dir }}"
|
||||
echo "⏱️ Build time: ${{ steps.generate.outputs.build-time }}s"
|
||||
if [ -d "${{ inputs.output-dir }}" ]; then
|
||||
echo "📄 Generated files:"
|
||||
find ${{ inputs.output-dir }} -type f | head -10
|
||||
fi
|
@@ -1 +0,0 @@
|
||||
あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。
|
Binary file not shown.
@@ -1,18 +0,0 @@
|
||||
tunnel: ec5a422d-7678-4e73-bf38-6105ffd4766a
|
||||
credentials-file: /Users/syui/.cloudflared/ec5a422d-7678-4e73-bf38-6105ffd4766a.json
|
||||
|
||||
ingress:
|
||||
- hostname: log.syui.ai
|
||||
service: http://localhost:4173
|
||||
originRequest:
|
||||
noHappyEyeballs: true
|
||||
|
||||
- hostname: ollama.syui.ai
|
||||
service: http://localhost:11434
|
||||
originRequest:
|
||||
noHappyEyeballs: true
|
||||
httpHostHeader: "localhost:11434"
|
||||
# Cloudflare Accessを無効化する場合は以下をコメントアウト
|
||||
# accessPolicy: bypass
|
||||
|
||||
- service: http_status:404
|
@@ -20,6 +20,7 @@ model = "gemma3:4b"
|
||||
host = "https://ollama.syui.ai"
|
||||
system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
|
||||
ai_did = "did:plc:4hqjfn7m6n5hno3doamuhgef"
|
||||
#num_predict = 200
|
||||
|
||||
[oauth]
|
||||
json = "client-metadata.json"
|
||||
|
@@ -57,24 +57,28 @@ $ npm run build
|
||||
$ npm run preview
|
||||
```
|
||||
|
||||
```sh
|
||||
```sh:ouath/.env.production
|
||||
# Production environment variables
|
||||
VITE_APP_HOST=https://example.com
|
||||
VITE_OAUTH_CLIENT_ID=https://example.com/client-metadata.json
|
||||
VITE_OAUTH_REDIRECT_URI=https://example.com/oauth/callback
|
||||
VITE_APP_HOST=https://syui.ai
|
||||
VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
|
||||
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
|
||||
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
||||
|
||||
# Collection names for OAuth app
|
||||
VITE_COLLECTION_COMMENT=ai.syui.log
|
||||
VITE_COLLECTION_USER=ai.syui.log.user
|
||||
VITE_COLLECTION_CHAT=ai.syui.log.chat
|
||||
# Base collection (all others are derived via getCollectionNames)
|
||||
VITE_OAUTH_COLLECTION=ai.syui.log
|
||||
|
||||
# Collection names for ailog (backward compatibility)
|
||||
AILOG_COLLECTION_COMMENT=ai.syui.log
|
||||
AILOG_COLLECTION_USER=ai.syui.log.user
|
||||
# AI Configuration
|
||||
VITE_AI_ENABLED=true
|
||||
VITE_AI_ASK_AI=true
|
||||
VITE_AI_PROVIDER=ollama
|
||||
VITE_AI_MODEL=gemma3:4b
|
||||
VITE_AI_HOST=https://ollama.syui.ai
|
||||
VITE_AI_SYSTEM_PROMPT="ai"
|
||||
VITE_AI_DID=did:plc:4hqjfn7m6n5hno3doamuhgef
|
||||
|
||||
# API Configuration
|
||||
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
|
||||
VITE_ATPROTO_API=https://bsky.social
|
||||
```
|
||||
|
||||
これは`ailog oauth build my-blog`で`./my-blog/config.toml`から`./oauth/.env.production`が生成されます。
|
||||
@@ -115,15 +119,8 @@ $ cloudflared tunnel --config cloudflared-config.yml run
|
||||
$ cloudflared tunnel route dns ${uuid} example.com
|
||||
```
|
||||
|
||||
以下の2つのcollection recordを生成します。ユーザーには`ai.syui.log`が生成され、ここにコメントが記録されます。それを取得して表示しています。`ai.syui.log.user`は管理者である`VITE_ADMIN_DID`用です。
|
||||
|
||||
```sh
|
||||
VITE_COLLECTION_COMMENT=ai.syui.log
|
||||
VITE_COLLECTION_USER=ai.syui.log.user
|
||||
```
|
||||
|
||||
```sh
|
||||
$ ailog auth login
|
||||
$ ailog auth init
|
||||
$ ailog stream server
|
||||
```
|
||||
|
||||
@@ -135,8 +132,9 @@ $ ailog stream server
|
||||
|
||||
`ask-AI`の仕組みは割愛します。後に変更される可能性が高いと思います。
|
||||
|
||||
local llm, mcp, atprotoと組み合わせです。
|
||||
`llm`, `mcp`, `atproto`などの組み合わせです。
|
||||
|
||||
現在、`/index.json`を監視して、更新があれば、翻訳などを行い自動ポストする機能があります。
|
||||
|
||||
## code syntax
|
||||
|
||||
|
@@ -248,7 +248,7 @@ a.view-markdown:any-link {
|
||||
}
|
||||
|
||||
.post-title a {
|
||||
color: #1f2328;
|
||||
color: var(--theme-color);
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
@@ -822,6 +822,13 @@ article.article-content {
|
||||
}
|
||||
|
||||
.comment-section {
|
||||
max-width: 100% !important;
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
||||
.comment-container {
|
||||
max-width: 100% !important;
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
@@ -1,3 +0,0 @@
|
||||
<!-- OAuth Comment System - Load globally for session management -->
|
||||
<script type="module" crossorigin src="/assets/comment-atproto-C3utAhPv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-BH-72ESb.css">
|
@@ -253,6 +253,20 @@ function setupAskAIEventListeners() {
|
||||
handleAIResponse(event.detail);
|
||||
});
|
||||
|
||||
// Track IME composition state
|
||||
let isComposing = false;
|
||||
const aiQuestionInput = document.getElementById('aiQuestion');
|
||||
|
||||
if (aiQuestionInput) {
|
||||
aiQuestionInput.addEventListener('compositionstart', function() {
|
||||
isComposing = true;
|
||||
});
|
||||
|
||||
aiQuestionInput.addEventListener('compositionend', function() {
|
||||
isComposing = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
@@ -262,8 +276,8 @@ function setupAskAIEventListeners() {
|
||||
}
|
||||
}
|
||||
|
||||
// Enter key to send message
|
||||
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey) {
|
||||
// Enter key to send message (only when not composing Japanese input)
|
||||
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey && !isComposing) {
|
||||
e.preventDefault();
|
||||
askQuestion();
|
||||
}
|
||||
|
@@ -20,19 +20,6 @@
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
</h3>
|
||||
|
||||
{% if post.excerpt %}
|
||||
<p class="post-excerpt">{{ post.excerpt }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="post-actions">
|
||||
<a href="{{ post.url }}" class="read-more">Read more</a>
|
||||
{% if post.markdown_url %}
|
||||
<a href="{{ post.markdown_url }}" class="view-markdown" title="View Markdown">.md</a>
|
||||
{% endif %}
|
||||
{% if post.translation_url %}
|
||||
<a href="{{ post.translation_url }}" class="view-translation" title="View Translation">🌐</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
|
@@ -1,3 +0,0 @@
|
||||
<!-- OAuth Comment System - Load globally for session management -->
|
||||
<script type="module" crossorigin src="/assets/comment-atproto-C3utAhPv.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-BH-72ESb.css">
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "aicard",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --mode development",
|
||||
|
@@ -168,7 +168,14 @@
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
* {
|
||||
max-width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
.app .app-main {
|
||||
max-width: 100% !important;
|
||||
margin: 0 !important;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
@@ -186,8 +193,8 @@
|
||||
}
|
||||
|
||||
.comment-section {
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
padding: 30px 0 !important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
@@ -209,6 +216,7 @@
|
||||
/* Ensure full width on mobile */
|
||||
.app {
|
||||
max-width: 100vw !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
/* Fix button overflow */
|
||||
@@ -324,6 +332,14 @@
|
||||
/* padding: 20px; - removed to avoid double padding */
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.comment-section {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.auth-section {
|
||||
background: #f8f9fa;
|
||||
@@ -334,6 +350,38 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-section.search-bar-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.auth-section.search-bar-layout .handle-input {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
padding: 10px 15px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px 0 0 6px;
|
||||
background: white;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.auth-section.search-bar-layout .handle-input:focus {
|
||||
border-color: var(--theme-color);
|
||||
}
|
||||
|
||||
.auth-section.search-bar-layout .atproto-button {
|
||||
margin: 0;
|
||||
padding: 10px 20px;
|
||||
border-radius: 0 6px 6px 0;
|
||||
min-width: 50px;
|
||||
font-weight: bold;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.atproto-button {
|
||||
background: var(--theme-color);
|
||||
color: var(--white);
|
||||
@@ -367,6 +415,30 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Override for search bar layout */
|
||||
.search-bar-layout .handle-input {
|
||||
width: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Mobile responsive for search bar */
|
||||
@media (max-width: 480px) {
|
||||
.auth-section.search-bar-layout {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.auth-section.search-bar-layout .handle-input {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.auth-section.search-bar-layout .atproto-button {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.auth-hint {
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
@@ -499,9 +571,8 @@
|
||||
}
|
||||
|
||||
.comments-list {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.comments-header {
|
||||
@@ -860,28 +931,6 @@
|
||||
background: #f6f8fa;
|
||||
}
|
||||
|
||||
/* AI Chat History */
|
||||
.ai-chat-list {
|
||||
max-width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
border: 1px solid #d1d9e0;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.chat-actions {
|
||||
display: flex;
|
||||
@@ -932,4 +981,8 @@
|
||||
padding: 40px 20px;
|
||||
color: #656d76;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message.comment-style {
|
||||
border-left: 4px solid var(--theme-color);
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ function App() {
|
||||
const [aiChatHistory, setAiChatHistory] = useState<any[]>([]);
|
||||
const [langEnRecords, setLangEnRecords] = useState<any[]>([]);
|
||||
const [aiCommentRecords, setAiCommentRecords] = useState<any[]>([]);
|
||||
const [aiProfile, setAiProfile] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Setup Jetstream WebSocket for real-time comments (optional)
|
||||
@@ -88,6 +89,20 @@ function App() {
|
||||
|
||||
// Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示)
|
||||
loadAiChatHistory();
|
||||
|
||||
// Load AI profile
|
||||
const fetchAiProfile = async () => {
|
||||
try {
|
||||
const response = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(appConfig.aiDid)}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAiProfile(data);
|
||||
}
|
||||
} catch (err) {
|
||||
// Use default values if fetch fails
|
||||
}
|
||||
};
|
||||
fetchAiProfile();
|
||||
|
||||
// Handle popstate events for mock OAuth flow
|
||||
const handlePopState = () => {
|
||||
@@ -259,8 +274,8 @@ function App() {
|
||||
if (appConfig.rkey) {
|
||||
// On post page: show only chats for this specific post
|
||||
filteredRecords = allChatRecords.filter(record => {
|
||||
const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname : '';
|
||||
return recordPath === window.location.pathname;
|
||||
const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
|
||||
return recordRkey === appConfig.rkey;
|
||||
});
|
||||
} else {
|
||||
// On top page: show latest 3 records from all pages
|
||||
@@ -302,13 +317,12 @@ function App() {
|
||||
const langData = await langResponse.json();
|
||||
const langRecords = langData.records || [];
|
||||
|
||||
// Filter by current page path if on post page
|
||||
// Filter by current page rkey if on post page
|
||||
const filteredLangRecords = appConfig.rkey
|
||||
? langRecords.filter(record => {
|
||||
// Compare path only, not full URL to support localhost vs production
|
||||
const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname :
|
||||
record.value.url ? new URL(record.value.url).pathname : '';
|
||||
return recordPath === window.location.pathname;
|
||||
// Compare rkey only (last part of path)
|
||||
const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
|
||||
return recordRkey === appConfig.rkey;
|
||||
})
|
||||
: langRecords.slice(0, 3); // Top page: latest 3
|
||||
|
||||
@@ -321,13 +335,12 @@ function App() {
|
||||
const commentData = await commentResponse.json();
|
||||
const commentRecords = commentData.records || [];
|
||||
|
||||
// Filter by current page path if on post page
|
||||
// Filter by current page rkey if on post page
|
||||
const filteredCommentRecords = appConfig.rkey
|
||||
? commentRecords.filter(record => {
|
||||
// Compare path only, not full URL to support localhost vs production
|
||||
const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname :
|
||||
record.value.url ? new URL(record.value.url).pathname : '';
|
||||
return recordPath === window.location.pathname;
|
||||
// Compare rkey only (last part of path)
|
||||
const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
|
||||
return recordRkey === appConfig.rkey;
|
||||
})
|
||||
: commentRecords.slice(0, 3); // Top page: latest 3
|
||||
|
||||
@@ -540,16 +553,14 @@ function App() {
|
||||
|
||||
|
||||
// ページpathでフィルタリング(指定された場合)
|
||||
const filteredComments = pageUrl
|
||||
const filteredComments = pageUrl && appConfig.rkey
|
||||
? userComments.filter(record => {
|
||||
try {
|
||||
// Compare path only, not full URL to support localhost vs production
|
||||
const recordPath = record.value.url ? new URL(record.value.url).pathname : '';
|
||||
const currentPath = new URL(pageUrl).pathname;
|
||||
return recordPath === currentPath;
|
||||
// Compare rkey only (last part of path)
|
||||
const recordRkey = record.value.url ? new URL(record.value.url).pathname.split('/').pop() : '';
|
||||
return recordRkey === appConfig.rkey;
|
||||
} catch (err) {
|
||||
// Fallback to exact match if URL parsing fails
|
||||
return record.value.url === pageUrl;
|
||||
return false;
|
||||
}
|
||||
})
|
||||
: userComments;
|
||||
@@ -970,7 +981,7 @@ function App() {
|
||||
{authorInfo?.displayName || 'AI'}
|
||||
</span>
|
||||
<span className="comment-handle">
|
||||
@{authorInfo?.handle || 'ai'}
|
||||
@{authorInfo?.handle || aiProfile?.handle || 'yui.syui.ai'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="comment-date">
|
||||
@@ -1043,28 +1054,28 @@ function App() {
|
||||
<section className="comment-section">
|
||||
{/* Authentication Section */}
|
||||
{!user ? (
|
||||
<div className="auth-section">
|
||||
<div className="auth-section search-bar-layout">
|
||||
<input
|
||||
type="text"
|
||||
id="handle-input"
|
||||
name="handle"
|
||||
placeholder="user.bsky.social"
|
||||
className="handle-input"
|
||||
value={handleInput}
|
||||
onChange={(e) => setHandleInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
executeOAuth();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={executeOAuth}
|
||||
className="atproto-button"
|
||||
>
|
||||
atproto
|
||||
<i class="fab fa-bluesky"></i>
|
||||
</button>
|
||||
<div className="username-input-section">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="user.bsky.social"
|
||||
className="handle-input"
|
||||
value={handleInput}
|
||||
onChange={(e) => setHandleInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
executeOAuth();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="user-section">
|
||||
@@ -1094,6 +1105,8 @@ function App() {
|
||||
{/* User List Form */}
|
||||
<div className="user-list-form">
|
||||
<textarea
|
||||
id="user-list-input"
|
||||
name="userList"
|
||||
value={userListInput}
|
||||
onChange={(e) => setUserListInput(e.target.value)}
|
||||
placeholder="ユーザーハンドルをカンマ区切りで入力 例: syui.ai, yui.syui.ai, user.bsky.social"
|
||||
@@ -1182,25 +1195,25 @@ function App() {
|
||||
className={`tab-button ${activeTab === 'comments' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('comments')}
|
||||
>
|
||||
Comments ({comments.filter(shouldShowComment).length})
|
||||
comment ({comments.filter(shouldShowComment).length})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'ai-chat' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('ai-chat')}
|
||||
>
|
||||
AI Chat History ({aiChatHistory.length})
|
||||
chat ({aiChatHistory.length})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'lang-en' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('lang-en')}
|
||||
>
|
||||
Lang: EN ({langEnRecords.length})
|
||||
en ({langEnRecords.length})
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'ai-comment' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('ai-comment')}
|
||||
>
|
||||
AI Comment ({aiCommentRecords.length})
|
||||
feedback ({aiCommentRecords.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1304,10 +1317,7 @@ function App() {
|
||||
|
||||
{/* AI Chat History List */}
|
||||
{activeTab === 'ai-chat' && (
|
||||
<div className="ai-chat-list">
|
||||
<div className="chat-header">
|
||||
<h3>AI Chat History</h3>
|
||||
</div>
|
||||
<div className="comments-list">
|
||||
{aiChatHistory.length === 0 ? (
|
||||
<p className="no-chat">No AI conversations yet. Start chatting with Ask AI!</p>
|
||||
) : (
|
||||
@@ -1315,12 +1325,12 @@ function App() {
|
||||
// For AI responses, use AI DID; for user questions, use the actual author
|
||||
const isAiResponse = record.value.type === 'answer';
|
||||
const displayDid = isAiResponse ? appConfig.aiDid : record.value.author?.did;
|
||||
const displayHandle = isAiResponse ? 'ai.syui' : record.value.author?.handle;
|
||||
const displayHandle = isAiResponse ? (aiProfile?.handle || 'yui.syui.ai') : record.value.author?.handle;
|
||||
const displayName = isAiResponse ? 'AI' : (record.value.author?.displayName || record.value.author?.handle);
|
||||
|
||||
return (
|
||||
<div key={index} className="chat-item">
|
||||
<div className="chat-header">
|
||||
<div key={index} className="comment-item">
|
||||
<div className="comment-header">
|
||||
<img
|
||||
src={generatePlaceholderAvatar(displayHandle || 'unknown')}
|
||||
alt={isAiResponse ? "AI Avatar" : "User Avatar"}
|
||||
@@ -1404,7 +1414,7 @@ function App() {
|
||||
|
||||
{/* Lang: EN List */}
|
||||
{activeTab === 'lang-en' && (
|
||||
<div className="lang-en-list">
|
||||
<div className="comments-list">
|
||||
{langEnRecords.length === 0 ? (
|
||||
<p className="no-content">No English translations yet</p>
|
||||
) : (
|
||||
@@ -1449,7 +1459,7 @@ function App() {
|
||||
AI
|
||||
</span>
|
||||
<span className="comment-handle">
|
||||
@ai
|
||||
@{aiProfile?.handle || 'yui.syui.ai'}
|
||||
</span>
|
||||
</div>
|
||||
<span className="comment-date">
|
||||
@@ -1496,11 +1506,12 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Comment Form - Only show on post pages */}
|
||||
{user && appConfig.rkey && (
|
||||
{/* Comment Form - Only show on post pages when Comments tab is active */}
|
||||
{user && appConfig.rkey && activeTab === 'comments' && (
|
||||
<div className="comment-form">
|
||||
<h3>Post a Comment</h3>
|
||||
<textarea
|
||||
id="comment-text"
|
||||
name="commentText"
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
placeholder="Write your comment..."
|
||||
@@ -1521,13 +1532,6 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show authentication status on non-post pages */}
|
||||
{user && !appConfig.rkey && (
|
||||
<div className="auth-status">
|
||||
<p>✅ Authenticated as @{user.handle}</p>
|
||||
<p><small>Visit a post page to comment</small></p>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1537,4 +1541,4 @@ function App() {
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
@@ -14,7 +14,7 @@ const response = await fetch(`${aiConfig.host}/api/generate`, {
|
||||
options: {
|
||||
temperature: 0.9,
|
||||
top_p: 0.9,
|
||||
num_predict: 80,
|
||||
num_predict: 200,
|
||||
repeat_penalty: 1.1,
|
||||
}
|
||||
}),
|
||||
|
@@ -199,7 +199,7 @@ Answer:`;
|
||||
options: {
|
||||
temperature: 0.9,
|
||||
top_p: 0.9,
|
||||
num_predict: 80, // Shorter responses for faster generation
|
||||
num_predict: 200, // Longer responses for better answers
|
||||
repeat_penalty: 1.1,
|
||||
}
|
||||
}),
|
||||
|
@@ -62,11 +62,15 @@ function generateBaseCollectionFromHost(host: string): string {
|
||||
}
|
||||
|
||||
// Extract rkey from current URL
|
||||
// /posts/xxx -> xxx
|
||||
// /posts/xxx -> xxx (remove .html if present)
|
||||
function extractRkeyFromUrl(): string | undefined {
|
||||
const pathname = window.location.pathname;
|
||||
const match = pathname.match(/\/posts\/([^/]+)\/?$/);
|
||||
return match ? match[1] : undefined;
|
||||
if (match) {
|
||||
// Remove .html extension if present
|
||||
return match[1].replace(/\.html$/, '');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get application configuration from environment variables
|
||||
|
4
bin/delete-chat-records.zsh → scpt/delete-chat-records.zsh
Executable file → Normal file
4
bin/delete-chat-records.zsh → scpt/delete-chat-records.zsh
Executable file → Normal file
@@ -1,10 +1,11 @@
|
||||
#!/bin/zsh
|
||||
|
||||
set -e
|
||||
|
||||
cb=ai.syui.log
|
||||
cl=( $cb.chat $cb.chat.comment $cb.chat.lang )
|
||||
|
||||
f=~/.config/syui/ai/bot/token.json
|
||||
|
||||
default_collection="ai.syui.log.chat.comment"
|
||||
default_pds="bsky.social"
|
||||
default_did=`cat $f|jq -r .did`
|
||||
@@ -18,7 +19,6 @@ did=${3:-$default_did}
|
||||
token=${4:-$default_token}
|
||||
req=com.atproto.repo.deleteRecord
|
||||
url=https://$pds/xrpc/$req
|
||||
|
||||
for i in $cl; do
|
||||
echo $i
|
||||
rkeys=($(curl -sL "https://$default_pds/xrpc/com.atproto.repo.listRecords?repo=$did&collection=$i&limit=100"|jq -r ".records[]?.uri"|cut -d '/' -f 5))
|
@@ -22,6 +22,7 @@ struct AiConfig {
|
||||
model: String,
|
||||
system_prompt: String,
|
||||
bsky_api: String,
|
||||
num_predict: Option<i32>,
|
||||
}
|
||||
|
||||
impl Default for AiConfig {
|
||||
@@ -33,6 +34,7 @@ impl Default for AiConfig {
|
||||
model: "gemma3:4b".to_string(),
|
||||
system_prompt: "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。".to_string(),
|
||||
bsky_api: "https://public.api.bsky.app".to_string(),
|
||||
num_predict: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,6 +195,11 @@ fn load_ai_config_from_project() -> Result<AiConfig> {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。")
|
||||
.to_string();
|
||||
|
||||
let num_predict = ai_config
|
||||
.and_then(|ai| ai.get("num_predict"))
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as i32);
|
||||
|
||||
// Extract OAuth config for bsky_api
|
||||
let oauth_config = config.get("oauth").and_then(|v| v.as_table());
|
||||
@@ -209,6 +216,7 @@ fn load_ai_config_from_project() -> Result<AiConfig> {
|
||||
model,
|
||||
system_prompt,
|
||||
bsky_api,
|
||||
num_predict,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1050,18 +1058,20 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ai_config: &AiCon
|
||||
};
|
||||
|
||||
format!(
|
||||
"{}\n\n# 指示\nこのブログ記事を読んで、アイらしい感想を一言でください。\n- 30文字以内の短い感想\n- 技術的な内容への素朴な驚きや発見\n- 「わー!」「すごい!」など、アイらしい感嘆詞で始める\n- 簡潔で分かりやすく\n\n# ブログ記事(要約)\n{}\n\n# 出力形式\n一言の感想のみ(説明や詳細は不要):",
|
||||
"{}\n\n# 指示\nこのブログ記事を読んで、アイらしい感想をください。\n- 100文字以内の感想\n- 技術的な内容への素朴な驚きや発見\n- アイらしい感嘆詞で始める\n- 簡潔で分かりやすく\n\n# ブログ記事(要約)\n{}\n\n# 出力形式\n感想のみ(説明や詳細は不要):",
|
||||
system_prompt, limited_content
|
||||
)
|
||||
},
|
||||
_ => return Err(anyhow::anyhow!("Unknown prompt type: {}", prompt_type)),
|
||||
};
|
||||
|
||||
let num_predict = match prompt_type {
|
||||
"comment" => 50, // Very short for comments (about 30-40 characters)
|
||||
"translate" => 3000, // Much longer for translations
|
||||
_ => 300,
|
||||
};
|
||||
let num_predict = ai_config.num_predict.unwrap_or_else(|| {
|
||||
match prompt_type {
|
||||
"comment" => 150, // Longer for comments (about 100 characters)
|
||||
"translate" => 3000, // Much longer for translations
|
||||
_ => 300,
|
||||
}
|
||||
});
|
||||
|
||||
let request = OllamaRequest {
|
||||
model: model.to_string(),
|
||||
@@ -1440,4 +1450,4 @@ async fn store_atproto_record(
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@@ -41,6 +41,7 @@ pub struct AiConfig {
|
||||
pub api_key: Option<String>,
|
||||
pub gpt_endpoint: Option<String>,
|
||||
pub atproto_config: Option<AtprotoConfig>,
|
||||
pub num_predict: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@@ -163,6 +164,7 @@ impl Default for Config {
|
||||
api_key: None,
|
||||
gpt_endpoint: None,
|
||||
atproto_config: None,
|
||||
num_predict: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@@ -6,9 +6,9 @@ Wants=network.target
|
||||
[Service]
|
||||
Type=simple
|
||||
User=syui
|
||||
Group=syui
|
||||
WorkingDirectory=/home/syui/git/log
|
||||
ExecStart=/home/syui/.cargo/bin/ailog stream start my-blog
|
||||
ExecStart=/home/syui/.cargo/bin/ailog stream start my-blog --ai-generate
|
||||
ExecStop=/home/syui/.cargo/bin/ailog stream stop
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
@@ -19,4 +19,4 @@ Environment=RUST_LOG=info
|
||||
Environment=AILOG_DEBUG_ALL=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
103
templates/api.md
103
templates/api.md
@@ -1,103 +0,0 @@
|
||||
# API Documentation
|
||||
|
||||
## Public Functions
|
||||
|
||||
{{#each api.public_functions}}
|
||||
### `{{this.name}}`
|
||||
|
||||
{{#if this.docs}}
|
||||
{{this.docs}}
|
||||
{{/if}}
|
||||
|
||||
**Visibility:** `{{this.visibility}}`
|
||||
{{#if this.is_async}}**Async:** Yes{{/if}}
|
||||
|
||||
{{#if this.parameters}}
|
||||
**Parameters:**
|
||||
{{#each this.parameters}}
|
||||
- `{{this.name}}`: `{{this.param_type}}`{{#if this.is_mutable}} (mutable){{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.return_type}}
|
||||
**Returns:** `{{this.return_type}}`
|
||||
{{/if}}
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Public Structs
|
||||
|
||||
{{#each api.public_structs}}
|
||||
### `{{this.name}}`
|
||||
|
||||
{{#if this.docs}}
|
||||
{{this.docs}}
|
||||
{{/if}}
|
||||
|
||||
**Visibility:** `{{this.visibility}}`
|
||||
|
||||
{{#if this.fields}}
|
||||
**Fields:**
|
||||
{{#each this.fields}}
|
||||
- `{{this.name}}`: `{{this.field_type}}` ({{this.visibility}})
|
||||
{{#if this.docs}} - {{this.docs}}{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Public Enums
|
||||
|
||||
{{#each api.public_enums}}
|
||||
### `{{this.name}}`
|
||||
|
||||
{{#if this.docs}}
|
||||
{{this.docs}}
|
||||
{{/if}}
|
||||
|
||||
**Visibility:** `{{this.visibility}}`
|
||||
|
||||
{{#if this.variants}}
|
||||
**Variants:**
|
||||
{{#each this.variants}}
|
||||
- `{{this.name}}`
|
||||
{{#if this.docs}} - {{this.docs}}{{/if}}
|
||||
{{#if this.fields}}
|
||||
**Fields:**
|
||||
{{#each this.fields}}
|
||||
- `{{this.name}}`: `{{this.field_type}}`
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Public Traits
|
||||
|
||||
{{#each api.public_traits}}
|
||||
### `{{this.name}}`
|
||||
|
||||
{{#if this.docs}}
|
||||
{{this.docs}}
|
||||
{{/if}}
|
||||
|
||||
**Visibility:** `{{this.visibility}}`
|
||||
|
||||
{{#if this.methods}}
|
||||
**Methods:**
|
||||
{{#each this.methods}}
|
||||
- `{{this.name}}({{#each this.parameters}}{{this.name}}: {{this.param_type}}{{#unless @last}}, {{/unless}}{{/each}}){{#if this.return_type}} -> {{this.return_type}}{{/if}}`
|
||||
{{#if this.docs}} - {{this.docs}}{{/if}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
@@ -1,19 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## Recent Changes
|
||||
|
||||
{{#each commits}}
|
||||
### {{this.date}}
|
||||
|
||||
**{{this.hash}}** by {{this.author}}
|
||||
|
||||
{{this.message}}
|
||||
|
||||
---
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Summary
|
||||
|
||||
- **Total Commits:** {{commits.length}}
|
||||
- **Contributors:** {{#unique commits "author"}}{{this.author}}{{#unless @last}}, {{/unless}}{{/unique}}
|
@@ -1,76 +0,0 @@
|
||||
# {{project.name}}
|
||||
|
||||
{{#if project.description}}
|
||||
{{project.description}}
|
||||
{{/if}}
|
||||
|
||||
## Overview
|
||||
|
||||
This project contains {{project.modules.length}} modules with a total of {{project.metrics.total_lines}} lines of code.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cargo install {{project.name}}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
{{project.name}} --help
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
{{#each project.dependencies}}
|
||||
- `{{@key}}`: {{this}}
|
||||
{{/each}}
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
{{#each project.structure.directories}}
|
||||
{{this.name}}/
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
{{#each project.modules}}
|
||||
### {{this.name}}
|
||||
|
||||
{{#if this.docs}}
|
||||
{{this.docs}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.functions}}
|
||||
**Functions:** {{this.functions.length}}
|
||||
{{/if}}
|
||||
|
||||
{{#if this.structs}}
|
||||
**Structs:** {{this.structs.length}}
|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
## Metrics
|
||||
|
||||
- **Lines of Code:** {{project.metrics.total_lines}}
|
||||
- **Total Files:** {{project.metrics.total_files}}
|
||||
- **Test Files:** {{project.metrics.test_files}}
|
||||
- **Dependencies:** {{project.metrics.dependency_count}}
|
||||
- **Complexity Score:** {{project.metrics.complexity_score}}
|
||||
|
||||
## License
|
||||
|
||||
{{#if project.license}}
|
||||
{{project.license}}
|
||||
{{else}}
|
||||
MIT
|
||||
{{/if}}
|
||||
|
||||
## Authors
|
||||
|
||||
{{#each project.authors}}
|
||||
- {{this}}
|
||||
{{/each}}
|
@@ -1,39 +0,0 @@
|
||||
# Project Structure
|
||||
|
||||
## Directory Overview
|
||||
|
||||
```
|
||||
{{#each structure.directories}}
|
||||
{{this.name}}/
|
||||
{{#each this.subdirectories}}
|
||||
├── {{this}}/
|
||||
{{/each}}
|
||||
{{#if this.file_count}}
|
||||
└── ({{this.file_count}} files)
|
||||
{{/if}}
|
||||
|
||||
{{/each}}
|
||||
```
|
||||
|
||||
## File Distribution
|
||||
|
||||
{{#each structure.files}}
|
||||
- **{{this.name}}** ({{this.language}}) - {{this.lines_of_code}} lines{{#if this.is_test}} [TEST]{{/if}}
|
||||
{{/each}}
|
||||
|
||||
## Statistics
|
||||
|
||||
- **Total Directories:** {{structure.directories.length}}
|
||||
- **Total Files:** {{structure.files.length}}
|
||||
- **Languages Used:**
|
||||
{{#group structure.files by="language"}}
|
||||
- {{@key}}: {{this.length}} files
|
||||
{{/group}}
|
||||
|
||||
{{#if structure.dependency_graph}}
|
||||
## Dependencies
|
||||
|
||||
{{#each structure.dependency_graph}}
|
||||
- **{{@key}}** depends on: {{#each this}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}
|
||||
{{/each}}
|
||||
{{/if}}
|
19
vercel.json
19
vercel.json
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"version": 2,
|
||||
"builds": [
|
||||
{
|
||||
"src": "my-blog/public/**",
|
||||
"use": "@vercel/static"
|
||||
}
|
||||
],
|
||||
"routes": [
|
||||
{
|
||||
"src": "/api/ask",
|
||||
"dest": "/api/ask.js"
|
||||
},
|
||||
{
|
||||
"src": "/(.*)",
|
||||
"dest": "/my-blog/public/$1"
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,93 +0,0 @@
|
||||
// Cloudflare Worker for secure Ollama proxy
|
||||
export default {
|
||||
async fetch(request, env, ctx) {
|
||||
// CORS preflight
|
||||
if (request.method === 'OPTIONS') {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': 'https://log.syui.ai',
|
||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, X-User-Token',
|
||||
'Access-Control-Max-Age': '86400',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Verify origin
|
||||
const origin = request.headers.get('Origin');
|
||||
const referer = request.headers.get('Referer');
|
||||
|
||||
// 許可されたオリジンのみ
|
||||
const allowedOrigins = [
|
||||
'https://log.syui.ai',
|
||||
'https://log.pages.dev' // Cloudflare Pages preview
|
||||
];
|
||||
|
||||
if (!origin || !allowedOrigins.some(allowed => origin.startsWith(allowed))) {
|
||||
return new Response('Forbidden', { status: 403 });
|
||||
}
|
||||
|
||||
// ユーザー認証トークン検証(オプション)
|
||||
const userToken = request.headers.get('X-User-Token');
|
||||
if (env.REQUIRE_AUTH && !userToken) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
// リクエストボディを取得
|
||||
const body = await request.json();
|
||||
|
||||
// プロンプトサイズ制限
|
||||
if (body.prompt && body.prompt.length > 1000) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Prompt too long. Maximum 1000 characters.'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// レート制限(CF Workers KV使用)
|
||||
if (env.RATE_LIMITER) {
|
||||
const clientIP = request.headers.get('CF-Connecting-IP');
|
||||
const rateLimitKey = `rate:${clientIP}`;
|
||||
const currentCount = await env.RATE_LIMITER.get(rateLimitKey) || 0;
|
||||
|
||||
if (currentCount >= 20) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Rate limit exceeded. Try again later.'
|
||||
}), {
|
||||
status: 429,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// カウント増加(1時間TTL)
|
||||
await env.RATE_LIMITER.put(rateLimitKey, currentCount + 1, {
|
||||
expirationTtl: 3600
|
||||
});
|
||||
}
|
||||
|
||||
// Ollamaへプロキシ
|
||||
const ollamaResponse = await fetch(env.OLLAMA_API_URL || 'https://ollama.syui.ai/api/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 内部認証ヘッダー(必要に応じて)
|
||||
'X-Internal-Token': env.OLLAMA_INTERNAL_TOKEN || ''
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
// レスポンスを返す
|
||||
const responseData = await ollamaResponse.text();
|
||||
|
||||
return new Response(responseData, {
|
||||
status: ollamaResponse.status,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Access-Control-Allow-Origin': origin,
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
@@ -1,20 +0,0 @@
|
||||
name = "ollama-proxy"
|
||||
main = "ollama-proxy.js"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
# 環境変数
|
||||
[vars]
|
||||
REQUIRE_AUTH = false
|
||||
|
||||
# 本番環境
|
||||
[env.production.vars]
|
||||
OLLAMA_API_URL = "https://ollama.syui.ai/api/generate"
|
||||
REQUIRE_AUTH = true
|
||||
|
||||
# KVネームスペース(レート制限用)
|
||||
[[kv_namespaces]]
|
||||
binding = "RATE_LIMITER"
|
||||
id = "your-kv-namespace-id"
|
||||
|
||||
# シークレット(wrangler secret putで設定)
|
||||
# OLLAMA_INTERNAL_TOKEN = "your-internal-token"
|
@@ -1,31 +0,0 @@
|
||||
name = "ailog"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
[env.production]
|
||||
name = "ailog"
|
||||
|
||||
[build]
|
||||
command = "cargo build --release && ./target/release/ailog build my-blog"
|
||||
publish = "my-blog/public"
|
||||
|
||||
[[redirects]]
|
||||
from = "/api/ask"
|
||||
to = "https://ai-gpt-mcp.your-domain.com/ask"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Frame-Options = "DENY"
|
||||
X-Content-Type-Options = "nosniff"
|
||||
Referrer-Policy = "strict-origin-when-cross-origin"
|
||||
|
||||
[[headers]]
|
||||
for = "/css/*"
|
||||
[headers.values]
|
||||
Cache-Control = "public, max-age=31536000, immutable"
|
||||
|
||||
[[headers]]
|
||||
for = "*.js"
|
||||
[headers.values]
|
||||
Cache-Control = "public, max-age=31536000, immutable"
|
Reference in New Issue
Block a user