Compare commits
14 Commits
v0.1.7
...
286b46c6e6
Author | SHA1 | Date | |
---|---|---|---|
286b46c6e6
|
|||
b780d27ace
|
|||
831fcb7865
|
|||
3f8bbff7c2
|
|||
5cb73a9ed3
|
|||
6ce8d44c4b
|
|||
167cfb35f7
|
|||
c8377ceabf
|
|||
e917c563f2
|
|||
a76933c23b
|
|||
8d960b7a40
|
|||
d3967c782f
|
|||
63b6fd5142
|
|||
27935324c7
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,7 +5,6 @@
|
|||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
cloudflare-config.yml
|
|
||||||
my-blog/public/
|
my-blog/public/
|
||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
@@ -13,3 +12,7 @@ package-lock.json
|
|||||||
my-blog/static/assets/comment-atproto-*
|
my-blog/static/assets/comment-atproto-*
|
||||||
bin/ailog
|
bin/ailog
|
||||||
docs
|
docs
|
||||||
|
my-blog/static/index.html
|
||||||
|
my-blog/templates/oauth-assets.html
|
||||||
|
cloudflared-config.yml
|
||||||
|
.config
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ailog"
|
name = "ailog"
|
||||||
version = "0.1.7"
|
version = "0.1.8"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["syui"]
|
authors = ["syui"]
|
||||||
description = "A static blog generator with AI features"
|
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"
|
host = "https://ollama.syui.ai"
|
||||||
system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
|
system_prompt = "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
|
||||||
ai_did = "did:plc:4hqjfn7m6n5hno3doamuhgef"
|
ai_did = "did:plc:4hqjfn7m6n5hno3doamuhgef"
|
||||||
|
#num_predict = 200
|
||||||
|
|
||||||
[oauth]
|
[oauth]
|
||||||
json = "client-metadata.json"
|
json = "client-metadata.json"
|
||||||
|
@@ -57,24 +57,28 @@ $ npm run build
|
|||||||
$ npm run preview
|
$ npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
```sh:ouath/.env.production
|
||||||
# Production environment variables
|
# Production environment variables
|
||||||
VITE_APP_HOST=https://example.com
|
VITE_APP_HOST=https://syui.ai
|
||||||
VITE_OAUTH_CLIENT_ID=https://example.com/client-metadata.json
|
VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
|
||||||
VITE_OAUTH_REDIRECT_URI=https://example.com/oauth/callback
|
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
|
||||||
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
||||||
|
|
||||||
# Collection names for OAuth app
|
# Base collection (all others are derived via getCollectionNames)
|
||||||
VITE_COLLECTION_COMMENT=ai.syui.log
|
VITE_OAUTH_COLLECTION=ai.syui.log
|
||||||
VITE_COLLECTION_USER=ai.syui.log.user
|
|
||||||
VITE_COLLECTION_CHAT=ai.syui.log.chat
|
|
||||||
|
|
||||||
# Collection names for ailog (backward compatibility)
|
# AI Configuration
|
||||||
AILOG_COLLECTION_COMMENT=ai.syui.log
|
VITE_AI_ENABLED=true
|
||||||
AILOG_COLLECTION_USER=ai.syui.log.user
|
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
|
# API Configuration
|
||||||
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
|
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`が生成されます。
|
これは`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
|
$ cloudflared tunnel route dns ${uuid} example.com
|
||||||
```
|
```
|
||||||
|
|
||||||
以下の2つのcollection recordを生成します。ユーザーには`ai.syui.log`が生成され、ここにコメントが記録されます。それを取得して表示しています。`ai.syui.log.user`は管理者である`VITE_ADMIN_DID`用です。
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
VITE_COLLECTION_COMMENT=ai.syui.log
|
$ ailog auth init
|
||||||
VITE_COLLECTION_USER=ai.syui.log.user
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ ailog auth login
|
|
||||||
$ ailog stream server
|
$ ailog stream server
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -135,8 +132,9 @@ $ ailog stream server
|
|||||||
|
|
||||||
`ask-AI`の仕組みは割愛します。後に変更される可能性が高いと思います。
|
`ask-AI`の仕組みは割愛します。後に変更される可能性が高いと思います。
|
||||||
|
|
||||||
local llm, mcp, atprotoと組み合わせです。
|
`llm`, `mcp`, `atproto`などの組み合わせです。
|
||||||
|
|
||||||
|
現在、`/index.json`を監視して、更新があれば、翻訳などを行い自動ポストする機能があります。
|
||||||
|
|
||||||
## code syntax
|
## code syntax
|
||||||
|
|
||||||
|
@@ -248,7 +248,7 @@ a.view-markdown:any-link {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post-title a {
|
.post-title a {
|
||||||
color: #1f2328;
|
color: var(--theme-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -822,6 +822,13 @@ article.article-content {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comment-section {
|
.comment-section {
|
||||||
|
max-width: 100% !important;
|
||||||
|
padding: 0px !important;
|
||||||
|
margin: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-container {
|
||||||
|
max-width: 100% !important;
|
||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
margin: 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-mfW-OeY_.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-Cm5qR-aM.css">
|
|
@@ -253,6 +253,20 @@ function setupAskAIEventListeners() {
|
|||||||
handleAIResponse(event.detail);
|
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
|
// Keyboard shortcuts
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
@@ -263,7 +277,7 @@ function setupAskAIEventListeners() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enter key to send message (only when not composing Japanese input)
|
// Enter key to send message (only when not composing Japanese input)
|
||||||
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey && !e.isComposing) {
|
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey && !isComposing) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
askQuestion();
|
askQuestion();
|
||||||
}
|
}
|
||||||
|
@@ -20,19 +20,6 @@
|
|||||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||||
</h3>
|
</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>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@@ -1,3 +0,0 @@
|
|||||||
<!-- OAuth Comment System - Load globally for session management -->
|
|
||||||
<script type="module" crossorigin src="/assets/comment-atproto-mfW-OeY_.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/comment-atproto-Cm5qR-aM.css">
|
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aicard",
|
"name": "aicard",
|
||||||
"version": "0.1.0",
|
"version": "0.1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --mode development",
|
"dev": "vite --mode development",
|
||||||
|
@@ -168,7 +168,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
|
* {
|
||||||
|
max-width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
.app .app-main {
|
.app .app-main {
|
||||||
|
max-width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +193,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comment-section {
|
.comment-section {
|
||||||
padding: 0px !important;
|
padding: 30px 0 !important;
|
||||||
margin: 0px !important;
|
margin: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,6 +216,7 @@
|
|||||||
/* Ensure full width on mobile */
|
/* Ensure full width on mobile */
|
||||||
.app {
|
.app {
|
||||||
max-width: 100vw !important;
|
max-width: 100vw !important;
|
||||||
|
overflow-x: hidden !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix button overflow */
|
/* Fix button overflow */
|
||||||
@@ -324,6 +332,14 @@
|
|||||||
/* padding: 20px; - removed to avoid double padding */
|
/* padding: 20px; - removed to avoid double padding */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.comment-section {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.auth-section {
|
.auth-section {
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
@@ -334,6 +350,38 @@
|
|||||||
text-align: center;
|
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 {
|
.atproto-button {
|
||||||
background: var(--theme-color);
|
background: var(--theme-color);
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
@@ -367,6 +415,30 @@
|
|||||||
text-align: center;
|
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 {
|
.auth-hint {
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@@ -28,6 +28,7 @@ function App() {
|
|||||||
const [aiChatHistory, setAiChatHistory] = useState<any[]>([]);
|
const [aiChatHistory, setAiChatHistory] = useState<any[]>([]);
|
||||||
const [langEnRecords, setLangEnRecords] = useState<any[]>([]);
|
const [langEnRecords, setLangEnRecords] = useState<any[]>([]);
|
||||||
const [aiCommentRecords, setAiCommentRecords] = useState<any[]>([]);
|
const [aiCommentRecords, setAiCommentRecords] = useState<any[]>([]);
|
||||||
|
const [aiProfile, setAiProfile] = useState<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Setup Jetstream WebSocket for real-time comments (optional)
|
// Setup Jetstream WebSocket for real-time comments (optional)
|
||||||
@@ -89,6 +90,20 @@ function App() {
|
|||||||
// Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示)
|
// Load AI chat history (認証状態に関係なく、全ユーザーのチャット履歴を表示)
|
||||||
loadAiChatHistory();
|
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
|
// Handle popstate events for mock OAuth flow
|
||||||
const handlePopState = () => {
|
const handlePopState = () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -966,7 +981,7 @@ function App() {
|
|||||||
{authorInfo?.displayName || 'AI'}
|
{authorInfo?.displayName || 'AI'}
|
||||||
</span>
|
</span>
|
||||||
<span className="comment-handle">
|
<span className="comment-handle">
|
||||||
@{authorInfo?.handle || 'ai'}
|
@{authorInfo?.handle || aiProfile?.handle || 'yui.syui.ai'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="comment-date">
|
<span className="comment-date">
|
||||||
@@ -1039,14 +1054,7 @@ function App() {
|
|||||||
<section className="comment-section">
|
<section className="comment-section">
|
||||||
{/* Authentication Section */}
|
{/* Authentication Section */}
|
||||||
{!user ? (
|
{!user ? (
|
||||||
<div className="auth-section">
|
<div className="auth-section search-bar-layout">
|
||||||
<button
|
|
||||||
onClick={executeOAuth}
|
|
||||||
className="atproto-button"
|
|
||||||
>
|
|
||||||
atproto
|
|
||||||
</button>
|
|
||||||
<div className="username-input-section">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="handle-input"
|
id="handle-input"
|
||||||
@@ -1062,7 +1070,12 @@ function App() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
<button
|
||||||
|
onClick={executeOAuth}
|
||||||
|
className="atproto-button"
|
||||||
|
>
|
||||||
|
<i class="fab fa-bluesky"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="user-section">
|
<div className="user-section">
|
||||||
@@ -1182,25 +1195,25 @@ function App() {
|
|||||||
className={`tab-button ${activeTab === 'comments' ? 'active' : ''}`}
|
className={`tab-button ${activeTab === 'comments' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('comments')}
|
onClick={() => setActiveTab('comments')}
|
||||||
>
|
>
|
||||||
Comments ({comments.filter(shouldShowComment).length})
|
comment ({comments.filter(shouldShowComment).length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab-button ${activeTab === 'ai-chat' ? 'active' : ''}`}
|
className={`tab-button ${activeTab === 'ai-chat' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('ai-chat')}
|
onClick={() => setActiveTab('ai-chat')}
|
||||||
>
|
>
|
||||||
AI Chat ({aiChatHistory.length})
|
chat ({aiChatHistory.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab-button ${activeTab === 'lang-en' ? 'active' : ''}`}
|
className={`tab-button ${activeTab === 'lang-en' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('lang-en')}
|
onClick={() => setActiveTab('lang-en')}
|
||||||
>
|
>
|
||||||
AI Lang:en ({langEnRecords.length})
|
en ({langEnRecords.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`tab-button ${activeTab === 'ai-comment' ? 'active' : ''}`}
|
className={`tab-button ${activeTab === 'ai-comment' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('ai-comment')}
|
onClick={() => setActiveTab('ai-comment')}
|
||||||
>
|
>
|
||||||
AI Comment ({aiCommentRecords.length})
|
feedback ({aiCommentRecords.length})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1312,7 +1325,7 @@ function App() {
|
|||||||
// For AI responses, use AI DID; for user questions, use the actual author
|
// For AI responses, use AI DID; for user questions, use the actual author
|
||||||
const isAiResponse = record.value.type === 'answer';
|
const isAiResponse = record.value.type === 'answer';
|
||||||
const displayDid = isAiResponse ? appConfig.aiDid : record.value.author?.did;
|
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);
|
const displayName = isAiResponse ? 'AI' : (record.value.author?.displayName || record.value.author?.handle);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1446,7 +1459,7 @@ function App() {
|
|||||||
AI
|
AI
|
||||||
</span>
|
</span>
|
||||||
<span className="comment-handle">
|
<span className="comment-handle">
|
||||||
@ai
|
@{aiProfile?.handle || 'yui.syui.ai'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="comment-date">
|
<span className="comment-date">
|
||||||
@@ -1519,13 +1532,6 @@ function App() {
|
|||||||
</div>
|
</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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
@@ -14,7 +14,7 @@ const response = await fetch(`${aiConfig.host}/api/generate`, {
|
|||||||
options: {
|
options: {
|
||||||
temperature: 0.9,
|
temperature: 0.9,
|
||||||
top_p: 0.9,
|
top_p: 0.9,
|
||||||
num_predict: 80,
|
num_predict: 200,
|
||||||
repeat_penalty: 1.1,
|
repeat_penalty: 1.1,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@@ -199,7 +199,7 @@ Answer:`;
|
|||||||
options: {
|
options: {
|
||||||
temperature: 0.9,
|
temperature: 0.9,
|
||||||
top_p: 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,
|
repeat_penalty: 1.1,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
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
|
#!/bin/zsh
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
cb=ai.syui.log
|
cb=ai.syui.log
|
||||||
cl=( $cb.chat $cb.chat.comment $cb.chat.lang )
|
cl=( $cb.chat $cb.chat.comment $cb.chat.lang )
|
||||||
|
|
||||||
f=~/.config/syui/ai/bot/token.json
|
f=~/.config/syui/ai/bot/token.json
|
||||||
|
|
||||||
default_collection="ai.syui.log.chat.comment"
|
default_collection="ai.syui.log.chat.comment"
|
||||||
default_pds="bsky.social"
|
default_pds="bsky.social"
|
||||||
default_did=`cat $f|jq -r .did`
|
default_did=`cat $f|jq -r .did`
|
||||||
@@ -18,7 +19,6 @@ did=${3:-$default_did}
|
|||||||
token=${4:-$default_token}
|
token=${4:-$default_token}
|
||||||
req=com.atproto.repo.deleteRecord
|
req=com.atproto.repo.deleteRecord
|
||||||
url=https://$pds/xrpc/$req
|
url=https://$pds/xrpc/$req
|
||||||
|
|
||||||
for i in $cl; do
|
for i in $cl; do
|
||||||
echo $i
|
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))
|
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,
|
model: String,
|
||||||
system_prompt: String,
|
system_prompt: String,
|
||||||
bsky_api: String,
|
bsky_api: String,
|
||||||
|
num_predict: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AiConfig {
|
impl Default for AiConfig {
|
||||||
@@ -33,6 +34,7 @@ impl Default for AiConfig {
|
|||||||
model: "gemma3:4b".to_string(),
|
model: "gemma3:4b".to_string(),
|
||||||
system_prompt: "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。".to_string(),
|
system_prompt: "あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。".to_string(),
|
||||||
bsky_api: "https://public.api.bsky.app".to_string(),
|
bsky_api: "https://public.api.bsky.app".to_string(),
|
||||||
|
num_predict: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,6 +196,11 @@ fn load_ai_config_from_project() -> Result<AiConfig> {
|
|||||||
.unwrap_or("あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。")
|
.unwrap_or("あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。相手のことが大好きで、ときどき甘えたり、照れたりします。でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。")
|
||||||
.to_string();
|
.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
|
// Extract OAuth config for bsky_api
|
||||||
let oauth_config = config.get("oauth").and_then(|v| v.as_table());
|
let oauth_config = config.get("oauth").and_then(|v| v.as_table());
|
||||||
let bsky_api = oauth_config
|
let bsky_api = oauth_config
|
||||||
@@ -209,6 +216,7 @@ fn load_ai_config_from_project() -> Result<AiConfig> {
|
|||||||
model,
|
model,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
bsky_api,
|
bsky_api,
|
||||||
|
num_predict,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1050,18 +1058,20 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ai_config: &AiCon
|
|||||||
};
|
};
|
||||||
|
|
||||||
format!(
|
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
|
system_prompt, limited_content
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
_ => return Err(anyhow::anyhow!("Unknown prompt type: {}", prompt_type)),
|
_ => return Err(anyhow::anyhow!("Unknown prompt type: {}", prompt_type)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let num_predict = match prompt_type {
|
let num_predict = ai_config.num_predict.unwrap_or_else(|| {
|
||||||
"comment" => 50, // Very short for comments (about 30-40 characters)
|
match prompt_type {
|
||||||
|
"comment" => 150, // Longer for comments (about 100 characters)
|
||||||
"translate" => 3000, // Much longer for translations
|
"translate" => 3000, // Much longer for translations
|
||||||
_ => 300,
|
_ => 300,
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let request = OllamaRequest {
|
let request = OllamaRequest {
|
||||||
model: model.to_string(),
|
model: model.to_string(),
|
||||||
|
@@ -41,6 +41,7 @@ pub struct AiConfig {
|
|||||||
pub api_key: Option<String>,
|
pub api_key: Option<String>,
|
||||||
pub gpt_endpoint: Option<String>,
|
pub gpt_endpoint: Option<String>,
|
||||||
pub atproto_config: Option<AtprotoConfig>,
|
pub atproto_config: Option<AtprotoConfig>,
|
||||||
|
pub num_predict: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
@@ -163,6 +164,7 @@ impl Default for Config {
|
|||||||
api_key: None,
|
api_key: None,
|
||||||
gpt_endpoint: None,
|
gpt_endpoint: None,
|
||||||
atproto_config: None,
|
atproto_config: None,
|
||||||
|
num_predict: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,9 @@ Wants=network.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=syui
|
User=syui
|
||||||
Group=syui
|
|
||||||
WorkingDirectory=/home/syui/git/log
|
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
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
|
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