Compare commits
4 Commits
3f8bbff7c2
...
v0.1.6
Author | SHA1 | Date | |
---|---|---|---|
5d97576544
|
|||
d16b88a499
|
|||
4df7f72312
|
|||
af28cefba0
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,6 +5,7 @@
|
|||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
cloudflare-config.yml
|
||||||
my-blog/public/
|
my-blog/public/
|
||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
@@ -12,7 +13,3 @@ 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.6"
|
||||||
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
Normal file
32
Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 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
Normal file
128
action.yml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
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
ai_prompt.txt
Normal file
1
ai_prompt.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。
|
Binary file not shown.
4
scpt/delete-chat-records.zsh → bin/delete-chat-records.zsh
Normal file → Executable file
4
scpt/delete-chat-records.zsh → bin/delete-chat-records.zsh
Normal file → Executable file
@@ -1,11 +1,10 @@
|
|||||||
#!/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`
|
||||||
@@ -19,6 +18,7 @@ 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))
|
18
cloudflared-config.yml
Normal file
18
cloudflared-config.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
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,7 +20,6 @@ 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,28 +57,24 @@ $ npm run build
|
|||||||
$ npm run preview
|
$ npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh:ouath/.env.production
|
```sh
|
||||||
# Production environment variables
|
# Production environment variables
|
||||||
VITE_APP_HOST=https://syui.ai
|
VITE_APP_HOST=https://example.com
|
||||||
VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
|
VITE_OAUTH_CLIENT_ID=https://example.com/client-metadata.json
|
||||||
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
|
VITE_OAUTH_REDIRECT_URI=https://example.com/oauth/callback
|
||||||
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
|
||||||
|
|
||||||
# Base collection (all others are derived via getCollectionNames)
|
# Collection names for OAuth app
|
||||||
VITE_OAUTH_COLLECTION=ai.syui.log
|
VITE_COLLECTION_COMMENT=ai.syui.log
|
||||||
|
VITE_COLLECTION_USER=ai.syui.log.user
|
||||||
|
VITE_COLLECTION_CHAT=ai.syui.log.chat
|
||||||
|
|
||||||
# AI Configuration
|
# Collection names for ailog (backward compatibility)
|
||||||
VITE_AI_ENABLED=true
|
AILOG_COLLECTION_COMMENT=ai.syui.log
|
||||||
VITE_AI_ASK_AI=true
|
AILOG_COLLECTION_USER=ai.syui.log.user
|
||||||
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`が生成されます。
|
||||||
@@ -119,8 +115,15 @@ $ 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
|
||||||
$ ailog auth init
|
VITE_COLLECTION_COMMENT=ai.syui.log
|
||||||
|
VITE_COLLECTION_USER=ai.syui.log.user
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ ailog auth login
|
||||||
$ ailog stream server
|
$ ailog stream server
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -132,9 +135,8 @@ $ ailog stream server
|
|||||||
|
|
||||||
`ask-AI`の仕組みは割愛します。後に変更される可能性が高いと思います。
|
`ask-AI`の仕組みは割愛します。後に変更される可能性が高いと思います。
|
||||||
|
|
||||||
`llm`, `mcp`, `atproto`などの組み合わせです。
|
local 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: var(--theme-color);
|
color: #1f2328;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -822,13 +822,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
3
my-blog/static/index.html
Normal file
3
my-blog/static/index.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<!-- 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,20 +253,6 @@ 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') {
|
||||||
@@ -276,8 +262,8 @@ function setupAskAIEventListeners() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter key to send message (only when not composing Japanese input)
|
// Enter key to send message
|
||||||
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey && !isComposing) {
|
if (e.key === 'Enter' && e.target.id === 'aiQuestion' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
askQuestion();
|
askQuestion();
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,19 @@
|
|||||||
<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 %}
|
||||||
|
3
my-blog/templates/oauth-assets.html
Normal file
3
my-blog/templates/oauth-assets.html
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<!-- 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">
|
@@ -168,14 +168,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +186,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comment-section {
|
.comment-section {
|
||||||
padding: 30px 0 !important;
|
padding: 0px !important;
|
||||||
margin: 0px !important;
|
margin: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-content {
|
.comment-content {
|
||||||
@@ -216,7 +209,6 @@
|
|||||||
/* 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 */
|
||||||
@@ -332,14 +324,6 @@
|
|||||||
/* 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;
|
||||||
@@ -350,38 +334,6 @@
|
|||||||
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);
|
||||||
@@ -415,30 +367,6 @@
|
|||||||
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;
|
||||||
@@ -571,8 +499,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.comments-list {
|
.comments-list {
|
||||||
|
border: 1px solid #ddd;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comments-header {
|
.comments-header {
|
||||||
@@ -931,6 +860,28 @@
|
|||||||
background: #f6f8fa;
|
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 {
|
.chat-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -981,8 +932,4 @@
|
|||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
color: #656d76;
|
color: #656d76;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message.comment-style {
|
|
||||||
border-left: 4px solid var(--theme-color);
|
|
||||||
}
|
|
@@ -28,7 +28,6 @@ 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,20 +88,6 @@ 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 = () => {
|
||||||
@@ -274,8 +259,8 @@ function App() {
|
|||||||
if (appConfig.rkey) {
|
if (appConfig.rkey) {
|
||||||
// On post page: show only chats for this specific post
|
// On post page: show only chats for this specific post
|
||||||
filteredRecords = allChatRecords.filter(record => {
|
filteredRecords = allChatRecords.filter(record => {
|
||||||
const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
|
const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname : '';
|
||||||
return recordRkey === appConfig.rkey;
|
return recordPath === window.location.pathname;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// On top page: show latest 3 records from all pages
|
// On top page: show latest 3 records from all pages
|
||||||
@@ -317,12 +302,13 @@ function App() {
|
|||||||
const langData = await langResponse.json();
|
const langData = await langResponse.json();
|
||||||
const langRecords = langData.records || [];
|
const langRecords = langData.records || [];
|
||||||
|
|
||||||
// Filter by current page rkey if on post page
|
// Filter by current page path if on post page
|
||||||
const filteredLangRecords = appConfig.rkey
|
const filteredLangRecords = appConfig.rkey
|
||||||
? langRecords.filter(record => {
|
? langRecords.filter(record => {
|
||||||
// Compare rkey only (last part of path)
|
// Compare path only, not full URL to support localhost vs production
|
||||||
const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
|
const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname :
|
||||||
return recordRkey === appConfig.rkey;
|
record.value.url ? new URL(record.value.url).pathname : '';
|
||||||
|
return recordPath === window.location.pathname;
|
||||||
})
|
})
|
||||||
: langRecords.slice(0, 3); // Top page: latest 3
|
: langRecords.slice(0, 3); // Top page: latest 3
|
||||||
|
|
||||||
@@ -335,12 +321,13 @@ function App() {
|
|||||||
const commentData = await commentResponse.json();
|
const commentData = await commentResponse.json();
|
||||||
const commentRecords = commentData.records || [];
|
const commentRecords = commentData.records || [];
|
||||||
|
|
||||||
// Filter by current page rkey if on post page
|
// Filter by current page path if on post page
|
||||||
const filteredCommentRecords = appConfig.rkey
|
const filteredCommentRecords = appConfig.rkey
|
||||||
? commentRecords.filter(record => {
|
? commentRecords.filter(record => {
|
||||||
// Compare rkey only (last part of path)
|
// Compare path only, not full URL to support localhost vs production
|
||||||
const recordRkey = record.value.post?.url ? new URL(record.value.post.url).pathname.split('/').pop()?.replace(/\.html$/, '') : '';
|
const recordPath = record.value.post?.url ? new URL(record.value.post.url).pathname :
|
||||||
return recordRkey === appConfig.rkey;
|
record.value.url ? new URL(record.value.url).pathname : '';
|
||||||
|
return recordPath === window.location.pathname;
|
||||||
})
|
})
|
||||||
: commentRecords.slice(0, 3); // Top page: latest 3
|
: commentRecords.slice(0, 3); // Top page: latest 3
|
||||||
|
|
||||||
@@ -553,14 +540,16 @@ function App() {
|
|||||||
|
|
||||||
|
|
||||||
// ページpathでフィルタリング(指定された場合)
|
// ページpathでフィルタリング(指定された場合)
|
||||||
const filteredComments = pageUrl && appConfig.rkey
|
const filteredComments = pageUrl
|
||||||
? userComments.filter(record => {
|
? userComments.filter(record => {
|
||||||
try {
|
try {
|
||||||
// Compare rkey only (last part of path)
|
// Compare path only, not full URL to support localhost vs production
|
||||||
const recordRkey = record.value.url ? new URL(record.value.url).pathname.split('/').pop() : '';
|
const recordPath = record.value.url ? new URL(record.value.url).pathname : '';
|
||||||
return recordRkey === appConfig.rkey;
|
const currentPath = new URL(pageUrl).pathname;
|
||||||
|
return recordPath === currentPath;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return false;
|
// Fallback to exact match if URL parsing fails
|
||||||
|
return record.value.url === pageUrl;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
: userComments;
|
: userComments;
|
||||||
@@ -981,7 +970,7 @@ function App() {
|
|||||||
{authorInfo?.displayName || 'AI'}
|
{authorInfo?.displayName || 'AI'}
|
||||||
</span>
|
</span>
|
||||||
<span className="comment-handle">
|
<span className="comment-handle">
|
||||||
@{authorInfo?.handle || aiProfile?.handle || 'yui.syui.ai'}
|
@{authorInfo?.handle || 'ai'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="comment-date">
|
<span className="comment-date">
|
||||||
@@ -1054,28 +1043,28 @@ function App() {
|
|||||||
<section className="comment-section">
|
<section className="comment-section">
|
||||||
{/* Authentication Section */}
|
{/* Authentication Section */}
|
||||||
{!user ? (
|
{!user ? (
|
||||||
<div className="auth-section search-bar-layout">
|
<div className="auth-section">
|
||||||
<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
|
<button
|
||||||
onClick={executeOAuth}
|
onClick={executeOAuth}
|
||||||
className="atproto-button"
|
className="atproto-button"
|
||||||
>
|
>
|
||||||
<i class="fab fa-bluesky"></i>
|
atproto
|
||||||
</button>
|
</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>
|
||||||
) : (
|
) : (
|
||||||
<div className="user-section">
|
<div className="user-section">
|
||||||
@@ -1105,8 +1094,6 @@ function App() {
|
|||||||
{/* User List Form */}
|
{/* User List Form */}
|
||||||
<div className="user-list-form">
|
<div className="user-list-form">
|
||||||
<textarea
|
<textarea
|
||||||
id="user-list-input"
|
|
||||||
name="userList"
|
|
||||||
value={userListInput}
|
value={userListInput}
|
||||||
onChange={(e) => setUserListInput(e.target.value)}
|
onChange={(e) => setUserListInput(e.target.value)}
|
||||||
placeholder="ユーザーハンドルをカンマ区切りで入力 例: syui.ai, yui.syui.ai, user.bsky.social"
|
placeholder="ユーザーハンドルをカンマ区切りで入力 例: syui.ai, yui.syui.ai, user.bsky.social"
|
||||||
@@ -1195,25 +1182,25 @@ function App() {
|
|||||||
className={`tab-button ${activeTab === 'comments' ? 'active' : ''}`}
|
className={`tab-button ${activeTab === 'comments' ? 'active' : ''}`}
|
||||||
onClick={() => setActiveTab('comments')}
|
onClick={() => setActiveTab('comments')}
|
||||||
>
|
>
|
||||||
comment ({comments.filter(shouldShowComment).length})
|
Comments ({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')}
|
||||||
>
|
>
|
||||||
chat ({aiChatHistory.length})
|
AI Chat History ({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')}
|
||||||
>
|
>
|
||||||
en ({langEnRecords.length})
|
Lang: 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')}
|
||||||
>
|
>
|
||||||
feedback ({aiCommentRecords.length})
|
AI Comment ({aiCommentRecords.length})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1317,7 +1304,10 @@ function App() {
|
|||||||
|
|
||||||
{/* AI Chat History List */}
|
{/* AI Chat History List */}
|
||||||
{activeTab === 'ai-chat' && (
|
{activeTab === 'ai-chat' && (
|
||||||
<div className="comments-list">
|
<div className="ai-chat-list">
|
||||||
|
<div className="chat-header">
|
||||||
|
<h3>AI Chat History</h3>
|
||||||
|
</div>
|
||||||
{aiChatHistory.length === 0 ? (
|
{aiChatHistory.length === 0 ? (
|
||||||
<p className="no-chat">No AI conversations yet. Start chatting with Ask AI!</p>
|
<p className="no-chat">No AI conversations yet. Start chatting with Ask AI!</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -1325,12 +1315,12 @@ 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 ? (aiProfile?.handle || 'yui.syui.ai') : record.value.author?.handle;
|
const displayHandle = isAiResponse ? 'ai.syui' : 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 (
|
||||||
<div key={index} className="comment-item">
|
<div key={index} className="chat-item">
|
||||||
<div className="comment-header">
|
<div className="chat-header">
|
||||||
<img
|
<img
|
||||||
src={generatePlaceholderAvatar(displayHandle || 'unknown')}
|
src={generatePlaceholderAvatar(displayHandle || 'unknown')}
|
||||||
alt={isAiResponse ? "AI Avatar" : "User Avatar"}
|
alt={isAiResponse ? "AI Avatar" : "User Avatar"}
|
||||||
@@ -1414,7 +1404,7 @@ function App() {
|
|||||||
|
|
||||||
{/* Lang: EN List */}
|
{/* Lang: EN List */}
|
||||||
{activeTab === 'lang-en' && (
|
{activeTab === 'lang-en' && (
|
||||||
<div className="comments-list">
|
<div className="lang-en-list">
|
||||||
{langEnRecords.length === 0 ? (
|
{langEnRecords.length === 0 ? (
|
||||||
<p className="no-content">No English translations yet</p>
|
<p className="no-content">No English translations yet</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -1459,7 +1449,7 @@ function App() {
|
|||||||
AI
|
AI
|
||||||
</span>
|
</span>
|
||||||
<span className="comment-handle">
|
<span className="comment-handle">
|
||||||
@{aiProfile?.handle || 'yui.syui.ai'}
|
@ai
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="comment-date">
|
<span className="comment-date">
|
||||||
@@ -1506,12 +1496,11 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Comment Form - Only show on post pages when Comments tab is active */}
|
{/* Comment Form - Only show on post pages */}
|
||||||
{user && appConfig.rkey && activeTab === 'comments' && (
|
{user && appConfig.rkey && (
|
||||||
<div className="comment-form">
|
<div className="comment-form">
|
||||||
|
<h3>Post a Comment</h3>
|
||||||
<textarea
|
<textarea
|
||||||
id="comment-text"
|
|
||||||
name="commentText"
|
|
||||||
value={commentText}
|
value={commentText}
|
||||||
onChange={(e) => setCommentText(e.target.value)}
|
onChange={(e) => setCommentText(e.target.value)}
|
||||||
placeholder="Write your comment..."
|
placeholder="Write your comment..."
|
||||||
@@ -1532,6 +1521,13 @@ 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>
|
||||||
|
|
||||||
@@ -1541,4 +1537,4 @@ function App() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
@@ -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: 200,
|
num_predict: 80,
|
||||||
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: 200, // Longer responses for better answers
|
num_predict: 80, // Shorter responses for faster generation
|
||||||
repeat_penalty: 1.1,
|
repeat_penalty: 1.1,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@@ -62,15 +62,11 @@ function generateBaseCollectionFromHost(host: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract rkey from current URL
|
// Extract rkey from current URL
|
||||||
// /posts/xxx -> xxx (remove .html if present)
|
// /posts/xxx -> xxx
|
||||||
function extractRkeyFromUrl(): string | undefined {
|
function extractRkeyFromUrl(): string | undefined {
|
||||||
const pathname = window.location.pathname;
|
const pathname = window.location.pathname;
|
||||||
const match = pathname.match(/\/posts\/([^/]+)\/?$/);
|
const match = pathname.match(/\/posts\/([^/]+)\/?$/);
|
||||||
if (match) {
|
return match ? match[1] : undefined;
|
||||||
// Remove .html extension if present
|
|
||||||
return match[1].replace(/\.html$/, '');
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get application configuration from environment variables
|
// Get application configuration from environment variables
|
||||||
|
@@ -22,7 +22,6 @@ 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 {
|
||||||
@@ -34,7 +33,6 @@ 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,11 +193,6 @@ fn load_ai_config_from_project() -> Result<AiConfig> {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.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());
|
||||||
@@ -216,7 +209,6 @@ fn load_ai_config_from_project() -> Result<AiConfig> {
|
|||||||
model,
|
model,
|
||||||
system_prompt,
|
system_prompt,
|
||||||
bsky_api,
|
bsky_api,
|
||||||
num_predict,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1058,20 +1050,18 @@ async fn generate_ai_content(content: &str, prompt_type: &str, ai_config: &AiCon
|
|||||||
};
|
};
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
"{}\n\n# 指示\nこのブログ記事を読んで、アイらしい感想をください。\n- 100文字以内の感想\n- 技術的な内容への素朴な驚きや発見\n- アイらしい感嘆詞で始める\n- 簡潔で分かりやすく\n\n# ブログ記事(要約)\n{}\n\n# 出力形式\n感想のみ(説明や詳細は不要):",
|
"{}\n\n# 指示\nこのブログ記事を読んで、アイらしい感想を一言でください。\n- 30文字以内の短い感想\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 = ai_config.num_predict.unwrap_or_else(|| {
|
let num_predict = match prompt_type {
|
||||||
match prompt_type {
|
"comment" => 50, // Very short for comments (about 30-40 characters)
|
||||||
"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(),
|
||||||
@@ -1450,4 +1440,4 @@ async fn store_atproto_record(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
@@ -41,7 +41,6 @@ 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)]
|
||||||
@@ -164,7 +163,6 @@ 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,
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@ Type=simple
|
|||||||
User=syui
|
User=syui
|
||||||
Group=syui
|
Group=syui
|
||||||
WorkingDirectory=/home/syui/git/log
|
WorkingDirectory=/home/syui/git/log
|
||||||
ExecStart=/home/syui/.cargo/bin/ailog stream start my-blog --ai-generate
|
ExecStart=/home/syui/.cargo/bin/ailog stream start my-blog
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
@@ -19,4 +19,4 @@ Environment=RUST_LOG=info
|
|||||||
Environment=AILOG_DEBUG_ALL=1
|
Environment=AILOG_DEBUG_ALL=1
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
103
templates/api.md
Normal file
103
templates/api.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# 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}}
|
19
templates/changelog.md
Normal file
19
templates/changelog.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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}}
|
76
templates/readme.md
Normal file
76
templates/readme.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# {{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}}
|
39
templates/structure.md
Normal file
39
templates/structure.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# 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
Normal file
19
vercel.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"builds": [
|
||||||
|
{
|
||||||
|
"src": "my-blog/public/**",
|
||||||
|
"use": "@vercel/static"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"src": "/api/ask",
|
||||||
|
"dest": "/api/ask.js"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/(.*)",
|
||||||
|
"dest": "/my-blog/public/$1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
93
workers/ollama-proxy.js
Normal file
93
workers/ollama-proxy.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// 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'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
20
workers/wrangler.toml
Normal file
20
workers/wrangler.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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"
|
31
wrangler.toml
Normal file
31
wrangler.toml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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