Upload 15 files
Browse files- # backend.yaml +11 -0
- README.md +7 -9
- backend:app:hf_utils.py +40 -0
- backend:app:main.py +61 -0
- docker-compose.yml +15 -0
- frontend:Dockerfile +7 -0
- frontend:package.json +22 -0
- frontend:scr:components:Field.ts +14 -0
- frontend:scr:components:GeneratorForm.tsx +103 -0
- frontend:scr:lib:api.ts +36 -0
- frontend:src:App.tsx +21 -0
- frontend:src:components:OutputPanel.tsx +31 -0
- frontend:src:main.tsx +5 -0
- frontend:tsconfig.json +13 -0
- frontend:vite.cofig.ts +7 -0
# backend.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# backend
|
| 2 |
+
cd backend
|
| 3 |
+
python -m venv .venv && source .venv/bin/activate
|
| 4 |
+
pip install -r requirements.txt
|
| 5 |
+
cp .env.example .env # キーを設定
|
| 6 |
+
uvicorn app.main:app --reload
|
| 7 |
+
|
| 8 |
+
# frontend
|
| 9 |
+
cd ../frontend
|
| 10 |
+
npm i
|
| 11 |
+
npm run dev
|
README.md
CHANGED
|
@@ -1,10 +1,8 @@
|
|
| 1 |
-
|
| 2 |
-
title: Sales
|
| 3 |
-
emoji: 📉
|
| 4 |
-
colorFrom: pink
|
| 5 |
-
colorTo: gray
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 営業メール/提案書ジェネレーター(HF + ChatGPT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
## 事前準備
|
| 4 |
+
- `backend/.env.example` を `backend/.env` にコピーし、`OPENAI_API_KEY` と `HF_API_KEY` を設定してください。
|
| 5 |
+
|
| 6 |
+
## ローカル起動(Docker)
|
| 7 |
+
```bash
|
| 8 |
+
docker compose up --build
|
backend:app:hf_utils.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
from .config import settings
|
| 3 |
+
|
| 4 |
+
HF_API_URL = "https://api-inference.huggingface.co/models"
|
| 5 |
+
|
| 6 |
+
async def hf_text_classification(model:str,text:str):
|
| 7 |
+
heders = {"authorization":f"Bearer{settings.hf_api_key}"}
|
| 8 |
+
async with httpx.AsyncClient(timeout=60) as client:
|
| 9 |
+
r = await client.post(f"{HF_API__URL}/{model}",headers=headers,json={"inputs":text})
|
| 10 |
+
r.raise_for_status()
|
| 11 |
+
return r.json()
|
| 12 |
+
|
| 13 |
+
async_def toxicity_score(text:str)->float:
|
| 14 |
+
try:
|
| 15 |
+
results = await hf_text_classification(settings.hf_toxic_model,text)
|
| 16 |
+
|
| 17 |
+
if isinstance(results,list)and results and isinstance(results[0],list):
|
| 18 |
+
|
| 19 |
+
labels = {d["label"].lower():d["score"]for d in results[0]}
|
| 20 |
+
return float (label.get("toxic",0.0))
|
| 21 |
+
elif isinstance(results,list)and results and "label" in results[0]:
|
| 22 |
+
return float(results[0]["score"])
|
| 23 |
+
except Exception:
|
| 24 |
+
pass
|
| 25 |
+
return 0.0
|
| 26 |
+
|
| 27 |
+
async def sentiment_polarity(text:str)->float:
|
| 28 |
+
try:
|
| 29 |
+
results = await hf_text_classification(settings.hf_sentiment_model)
|
| 30 |
+
|
| 31 |
+
if isinstance(results, list)and results and isinstance(result[0],list):
|
| 32 |
+
labels = {d["label"].upper():d["score"]for d in results[0]}
|
| 33 |
+
return float(labels.get("POSITIVE",0.0)-labels.get("NEAGATIVE",0.0))
|
| 34 |
+
elif isinstance(results,list)and results and "label" in results[0]:
|
| 35 |
+
label = results[0]["label"].upper()
|
| 36 |
+
score = results[0] ["score"]
|
| 37 |
+
return score if label == "POSITIVE" else (-score if label == "NEGATIVE" else 0.0)
|
| 38 |
+
except Exception:
|
| 39 |
+
pass
|
| 40 |
+
return 0.0
|
backend:app:main.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from .schemas import GenerateRequest, EmailResponse, PropasalResponse, LintRequest, LintResponse
|
| 4 |
+
from .prompt_teplates import EMAIL_SYSTEM, EMAIL_USER,PROPOSAL_SYSTEM,PROPOSAL_USER
|
| 5 |
+
from .openai_utils import chat_complete,split_subject_body
|
| 6 |
+
from .hf_utils import toxicity_score, sentiment_polarity
|
| 7 |
+
|
| 8 |
+
app = FastAPI(title="Salse Writer API", version ="0.1.0")
|
| 9 |
+
|
| 10 |
+
app.add_middleware(
|
| 11 |
+
CORMiddleware,
|
| 12 |
+
allow_origins=["http://localhost:5173"],
|
| 13 |
+
allow_credentials=True,
|
| 14 |
+
allow_methods=["*"],
|
| 15 |
+
allow_headers=["*"],
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
@app.get("/health")
|
| 19 |
+
def health():
|
| 20 |
+
rerutn{"status":"ok"}
|
| 21 |
+
|
| 22 |
+
@app.post("/generate/email",response_model=EmailResponse)
|
| 23 |
+
async def generate_email(req:GenerateRequest):
|
| 24 |
+
user_prompt = EMAIL_USER.format(**req.model_dump())
|
| 25 |
+
raw = chat_complete(EMAIL_SYSTEM,user_prompt)
|
| 26 |
+
subject,body =split_subject_body(raw)
|
| 27 |
+
|
| 28 |
+
tox = await toxicity_score(body)
|
| 29 |
+
sent = await sentiment_polarity(body)
|
| 30 |
+
warnings = []
|
| 31 |
+
if tox > 0.2:
|
| 32 |
+
warnings.append("トーンが攻撃的/不適切の可能性があります。表現を柔らかくしてください。")
|
| 33 |
+
|
| 34 |
+
quality={"toxicity":tox, "sentiment":sent}
|
| 35 |
+
return EmailResponse(subject=subject,body=body,quality=quality,warnings=warnings)
|
| 36 |
+
|
| 37 |
+
@app.post("/generate/proposal",response_model=ProposalResponse)
|
| 38 |
+
async def generate_proposal(req:GenerateRequest):
|
| 39 |
+
user_prompt = PROPOSAL_USER.format(**req.model_dump())
|
| 40 |
+
text = chat_complete(PROPOSAL_SYSTEM,user_prompt)
|
| 41 |
+
|
| 42 |
+
lines=[ln.strip("-• ").strip() for ln in text.splitlines()if ln.strip()]
|
| 43 |
+
outline = [ ln for ln in lines if not ln.startswith("エグゼクティブサマリー")]
|
| 44 |
+
summary=""
|
| 45 |
+
for i,ln in enumerate(lines):
|
| 46 |
+
if "エグゼクティブサマリー"in ln:
|
| 47 |
+
summary ="\n".join(lines[i+1:])
|
| 48 |
+
break
|
| 49 |
+
return ProposalResponse(outline=outline[:8],exective_summary=summary or "(サマリー抽出に失敗しました)")
|
| 50 |
+
|
| 51 |
+
@app.post("/lint", response_model=LintResponse)
|
| 52 |
+
async def lint_text(req:LintRequest):
|
| 53 |
+
tox =await toxicity_score(req.text)
|
| 54 |
+
issues=[]
|
| 55 |
+
if tox > 0.2:
|
| 56 |
+
issues.append("不適切・攻撃的な表現を含む可能性")
|
| 57 |
+
|
| 58 |
+
bad_patterns=["絶対に","必ず儲かる","今だけ","無料で全部"]
|
| 59 |
+
if any(p in req_text for p in bad_patterns):
|
| 60 |
+
issues.append("誇大広告とみなされる恐れのある表現")
|
| 61 |
+
return LintResponse(issues=issues, toxicity=tox)
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
api:
|
| 3 |
+
build: ./backend
|
| 4 |
+
env_file:
|
| 5 |
+
- ./backend/.env.example
|
| 6 |
+
ports:
|
| 7 |
+
- "8000:8000"
|
| 8 |
+
web:
|
| 9 |
+
build: ./frontend
|
| 10 |
+
environment:
|
| 11 |
+
- VITE_API_BASE=http://localhost:8000
|
| 12 |
+
ports:
|
| 13 |
+
- "5173:5173"
|
| 14 |
+
depends_on:
|
| 15 |
+
- api
|
frontend:Dockerfile
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:20-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY package.json package-lock.json* ./
|
| 4 |
+
RUN npm install
|
| 5 |
+
COPY . .
|
| 6 |
+
EXPOSE 5173
|
| 7 |
+
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
frontend:package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "sales-writer-frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"preview": "vite preview --port 5173"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"react": "^18.3.1",
|
| 13 |
+
"react-dom": "^18.3.1",
|
| 14 |
+
"zod": "^3.23.8"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@types/react": "^18.3.3",
|
| 18 |
+
"@types/react-dom": "^18.3.0",
|
| 19 |
+
"typescript": "^5.5.4",
|
| 20 |
+
"vite": "^5.4.2"
|
| 21 |
+
}
|
| 22 |
+
}
|
frontend:scr:components:Field.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
|
| 3 |
+
type Props = React.PropsWithChildren<{
|
| 4 |
+
label: string;
|
| 5 |
+
}>;
|
| 6 |
+
|
| 7 |
+
export default function Field({ label, children }: Props) {
|
| 8 |
+
return (
|
| 9 |
+
<label style={{ display: "block", marginBottom: 12 }}>
|
| 10 |
+
<div style={{ fontSize: 12, opacity: 0.8, marginBottom: 6 }}>{label}</div>
|
| 11 |
+
{children}
|
| 12 |
+
</label>
|
| 13 |
+
);
|
| 14 |
+
}
|
frontend:scr:components:GeneratorForm.tsx
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
+
import Field from "./Field";
|
| 3 |
+
import { generateEmail, lintText, GenerateReq } from "@/lib/api";
|
| 4 |
+
|
| 5 |
+
type Props = {
|
| 6 |
+
onResult: (r: any) => void;
|
| 7 |
+
};
|
| 8 |
+
|
| 9 |
+
export default function GeneratorForm({ onResult }: Props) {
|
| 10 |
+
const [loading, setLoading] = useState(false);
|
| 11 |
+
const [form, setForm] = useState<GenerateReq>({
|
| 12 |
+
language: "ja",
|
| 13 |
+
industry: "ITサービス",
|
| 14 |
+
target_company: "株式会社○○",
|
| 15 |
+
target_persona: "情報システム部の部長",
|
| 16 |
+
pain_points: "SaaS乱立によるコスト・運用負荷増大",
|
| 17 |
+
value_prop: "運用統合と可視化によりTCOを30%削減",
|
| 18 |
+
product_name: "Aroundabout Suite",
|
| 19 |
+
cta: "15分のオンライン面談をご提案",
|
| 20 |
+
tone: "フォーマルで簡潔",
|
| 21 |
+
variables: { company: "株式会社○○", person: "山田様" },
|
| 22 |
+
length_hint: "中",
|
| 23 |
+
extra_instr: ""
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
const update = (k: keyof GenerateReq) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
| 27 |
+
setForm(f => ({ ...f, [k]: e.target.value }));
|
| 28 |
+
|
| 29 |
+
async function onSubmit(e: React.FormEvent) {
|
| 30 |
+
e.preventDefault();
|
| 31 |
+
setLoading(true);
|
| 32 |
+
try {
|
| 33 |
+
const res = await generateEmail({ ...form, variables: form.variables });
|
| 34 |
+
// 追加の簡易Lint
|
| 35 |
+
const lint = await lintText(res.body);
|
| 36 |
+
onResult({ ...res, lint });
|
| 37 |
+
} catch (err) {
|
| 38 |
+
onResult({ error: String(err) });
|
| 39 |
+
} finally {
|
| 40 |
+
setLoading(false);
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return (
|
| 45 |
+
<form onSubmit={onSubmit} style={{ display: "grid", gap: 12 }}>
|
| 46 |
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
|
| 47 |
+
<Field label="言語">
|
| 48 |
+
<select value={form.language} onChange={update("language")}>
|
| 49 |
+
<option value="ja">日本語</option>
|
| 50 |
+
<option value="en">English</option>
|
| 51 |
+
</select>
|
| 52 |
+
</Field>
|
| 53 |
+
<Field label="トーン">
|
| 54 |
+
<input value={form.tone} onChange={update("tone")} />
|
| 55 |
+
</Field>
|
| 56 |
+
<Field label="業界">
|
| 57 |
+
<input value={form.industry} onChange={update("industry")} />
|
| 58 |
+
</Field>
|
| 59 |
+
<Field label="相手企業">
|
| 60 |
+
<input value={form.target_company} onChange={update("target_company")} />
|
| 61 |
+
</Field>
|
| 62 |
+
<Field label="担当者ペルソナ">
|
| 63 |
+
<input value={form.target_persona} onChange={update("target_persona")} />
|
| 64 |
+
</Field>
|
| 65 |
+
<Field label="文章ボリューム">
|
| 66 |
+
<select value={form.length_hint} onChange={update("length_hint")}>
|
| 67 |
+
<option>短</option><option>中</option><option>長</option>
|
| 68 |
+
</select>
|
| 69 |
+
</Field>
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
<Field label="相手の課題">
|
| 73 |
+
<textarea value={form.pain_points} onChange={update("pain_points")} rows={3} />
|
| 74 |
+
</Field>
|
| 75 |
+
<Field label="提供価値(成果/差別化)">
|
| 76 |
+
<textarea value={form.value_prop} onChange={update("value_prop")} rows={3} />
|
| 77 |
+
</Field>
|
| 78 |
+
<Field label="製品/サービス名">
|
| 79 |
+
<input value={form.product_name} onChange={update("product_name")} />
|
| 80 |
+
</Field>
|
| 81 |
+
<Field label="CTA(次アクション)">
|
| 82 |
+
<input value={form.cta} onChange={update("cta")} />
|
| 83 |
+
</Field>
|
| 84 |
+
<Field label="追加指示(禁止表現など)">
|
| 85 |
+
<input value={form.extra_instr ?? ""} onChange={update("extra_instr")} />
|
| 86 |
+
</Field>
|
| 87 |
+
|
| 88 |
+
<button
|
| 89 |
+
type="submit"
|
| 90 |
+
disabled={loading}
|
| 91 |
+
style={{
|
| 92 |
+
padding: "10px 14px",
|
| 93 |
+
borderRadius: 12,
|
| 94 |
+
border: "1px solid #ddd",
|
| 95 |
+
cursor: "pointer",
|
| 96 |
+
background: loading ? "#f3f3f3" : "white"
|
| 97 |
+
}}
|
| 98 |
+
>
|
| 99 |
+
{loading ? "生成中..." : "生成する"}
|
| 100 |
+
</button>
|
| 101 |
+
</form>
|
| 102 |
+
);
|
| 103 |
+
}
|
frontend:scr:lib:api.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export type GenerateReq = {
|
| 2 |
+
language: string;
|
| 3 |
+
industry: string;
|
| 4 |
+
target_company: string;
|
| 5 |
+
target_persona: string;
|
| 6 |
+
pain_points: string;
|
| 7 |
+
value_prop: string;
|
| 8 |
+
product_name: string;
|
| 9 |
+
cta: string;
|
| 10 |
+
tone: string;
|
| 11 |
+
variables: Record<string, string>;
|
| 12 |
+
length_hint: string;
|
| 13 |
+
extra_instr?: string | null;
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000";
|
| 17 |
+
|
| 18 |
+
export async function generateEmail(payload: GenerateReq) {
|
| 19 |
+
const r = await fetch(`${BASE}/generate/email`, {
|
| 20 |
+
method: "POST",
|
| 21 |
+
headers: { "Content-Type": "application/json" },
|
| 22 |
+
body: JSON.stringify(payload),
|
| 23 |
+
});
|
| 24 |
+
if (!r.ok) throw new Error(await r.text());
|
| 25 |
+
return r.json();
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
export async function lintText(text: string) {
|
| 29 |
+
const r = await fetch(`${BASE}/lint`, {
|
| 30 |
+
method: "POST",
|
| 31 |
+
headers: { "Content-Type": "application/json" },
|
| 32 |
+
body: JSON.stringify({ text }),
|
| 33 |
+
});
|
| 34 |
+
if (!r.ok) throw new Error(await r.text());
|
| 35 |
+
return r.json();
|
| 36 |
+
}
|
frontend:src:App.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from "react";
|
| 2 |
+
import GeneratorForm from "./components/GeneratorForm";
|
| 3 |
+
import OutputPanel from "./components/OutputPanel";
|
| 4 |
+
|
| 5 |
+
export default function App() {
|
| 6 |
+
const [data, setData] = useState<any | null>(null);
|
| 7 |
+
|
| 8 |
+
return (
|
| 9 |
+
<div style={{ maxWidth: 980, margin: "0 auto", padding: 24 }}>
|
| 10 |
+
<header style={{ marginBottom: 18 }}>
|
| 11 |
+
<h1 style={{ margin: 0 }}>営業メール / 提案書ジェネレーター</h1>
|
| 12 |
+
<div style={{ opacity: 0.7 }}>Hugging Face + ChatGPT(API連携MVP)</div>
|
| 13 |
+
</header>
|
| 14 |
+
<GeneratorForm onResult={setData} />
|
| 15 |
+
<OutputPanel data={data} />
|
| 16 |
+
<footer style={{ marginTop: 24, fontSize: 12, opacity: 0.6 }}>
|
| 17 |
+
※入力内容は生成のみに使用されます。保存は行いません(デモ設定)。
|
| 18 |
+
</footer>
|
| 19 |
+
</div>
|
| 20 |
+
);
|
| 21 |
+
}
|
frontend:src:components:OutputPanel.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
|
| 3 |
+
type Props = {
|
| 4 |
+
data: any | null;
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default function OutputPanel({ data }: Props) {
|
| 8 |
+
if (!data) return null;
|
| 9 |
+
if (data.error) return <div style={{ color: "crimson" }}>Error: {data.error}</div>;
|
| 10 |
+
|
| 11 |
+
return (
|
| 12 |
+
<div style={{ marginTop: 16, padding: 16, border: "1px solid #eee", borderRadius: 12 }}>
|
| 13 |
+
<h3 style={{ marginTop: 0 }}>結果</h3>
|
| 14 |
+
<div><strong>件名:</strong> {data.subject}</div>
|
| 15 |
+
<div style={{ whiteSpace: "pre-wrap", marginTop: 8 }}>{data.body}</div>
|
| 16 |
+
<div style={{ marginTop: 12, opacity: 0.8 }}>
|
| 17 |
+
<div>toxicity: {data.quality?.toxicity?.toFixed?.(3)}</div>
|
| 18 |
+
<div>sentiment: {data.quality?.sentiment?.toFixed?.(3)}</div>
|
| 19 |
+
{data.warnings?.length ? (
|
| 20 |
+
<ul>{data.warnings.map((w: string, i: number) => <li key={i}>{w}</li>)}</ul>
|
| 21 |
+
) : null}
|
| 22 |
+
{data.lint?.issues?.length ? (
|
| 23 |
+
<>
|
| 24 |
+
<div style={{ marginTop: 8 }}><strong>Lint:</strong></div>
|
| 25 |
+
<ul>{data.lint.issues.map((w: string, i: number) => <li key={i}>{w}</li>)}</ul>
|
| 26 |
+
</>
|
| 27 |
+
) : null}
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
);
|
| 31 |
+
}
|
frontend:src:main.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from "react";
|
| 2 |
+
import { createRoot } from "react-dom/client";
|
| 3 |
+
import App from "./App";
|
| 4 |
+
|
| 5 |
+
createRoot(document.getElementById("root")!).render(<App />);
|
frontend:tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2022",
|
| 4 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 5 |
+
"jsx": "react-jsx",
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"moduleResolution": "Bundler",
|
| 8 |
+
"strict": true,
|
| 9 |
+
"baseUrl": ".",
|
| 10 |
+
"paths": { "@/*": ["src/*"] }
|
| 11 |
+
},
|
| 12 |
+
"include": ["src"]
|
| 13 |
+
}
|
frontend:vite.cofig.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import{defineConfig}from "vite";
|
| 2 |
+
import react from "@vitejs/plugin-react";
|
| 3 |
+
|
| 4 |
+
export default defineConfig({
|
| 5 |
+
plugins:[react()],
|
| 6 |
+
server:{port:5173},
|
| 7 |
+
});
|