Spaces:
Runtime error
Runtime error
| # -*- coding: utf-8 -*- | |
| """app.py - マッチング理由機能を追加した企業文化マッチングアプリ(HuggingFace対応版)""" | |
| import sys | |
| import os | |
| import subprocess | |
| import logging | |
| # ロギング設定 | |
| logging.basicConfig(level=logging.INFO, | |
| format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | |
| handlers=[logging.StreamHandler()]) | |
| logger = logging.getLogger(__name__) | |
| # 必要なライブラリをインストール | |
| def install_requirements(): | |
| logger.info("必要なライブラリをインストールしています...") | |
| packages = ["gradio", "openai", "pandas", "numpy", "tabulate", "python-dotenv"] | |
| # requirements.txtの作成 | |
| with open("requirements.txt", "w") as f: | |
| f.write("\n".join(packages)) | |
| try: | |
| subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) | |
| logger.info("ライブラリのインストールが完了しました") | |
| except subprocess.CalledProcessError as e: | |
| logger.error(f"ライブラリのインストールに失敗しました: {e}") | |
| sys.exit(1) | |
| # 必要なライブラリがインストールされているか確認 | |
| try: | |
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| from openai import OpenAI | |
| from dotenv import load_dotenv | |
| except ImportError: | |
| install_requirements() | |
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| from openai import OpenAI | |
| from dotenv import load_dotenv | |
| import ast | |
| import time | |
| import traceback | |
| import json | |
| # 環境変数から APIキーを読み込む | |
| load_dotenv() | |
| OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") | |
| # APIキー用の.env.exampleファイルの作成 | |
| def create_env_example(): | |
| with open(".env.example", "w") as f: | |
| f.write("# このファイルを.envにリネームし、APIキーを設定してください\n") | |
| f.write("OPENAI_API_KEY=sk-あなたのAPIキーをここに入力\n") | |
| # README.mdファイルの作成 | |
| def create_readme(): | |
| with open("README.md", "w") as f: | |
| f.write("""# 企業文化マッチングアプリ | |
| ## 概要 | |
| このアプリケーションは、ユーザーの価値観と企業文化のマッチングを行います。 | |
| ## セットアップ | |
| 1. 必要なライブラリをインストールします: | |
| ``` | |
| pip install -r requirements.txt | |
| ``` | |
| 2. 環境変数を設定します: | |
| - `.env.example` ファイルを `.env` にリネームします | |
| - OpenAI APIキーを設定します | |
| ``` | |
| OPENAI_API_KEY=sk-あなたのAPIキーをここに入力 | |
| ``` | |
| 3. アプリケーションを起動します: | |
| ``` | |
| python app.py | |
| ``` | |
| ## Hugging Face Spacesでの設定 | |
| 1. このリポジトリをHugging Face Spacesにインポートします | |
| 2. Settings > Repository Secrets で以下の変数を設定します: | |
| - `OPENAI_API_KEY`: OpenAI APIキー | |
| ## 機能 | |
| - ユーザーの価値観を分析 | |
| - 最適な企業のマッチングを計算 | |
| - マッチング理由の生成 | |
| - 直感的なUI/UXデザイン | |
| """) | |
| # アプリケーション情報ログ出力 | |
| def log_app_info(): | |
| logger.info(f"Python version: {sys.version}") | |
| logger.info(f"Current working directory: {os.getcwd()}") | |
| logger.info(f"Directory contents: {os.listdir('.')}") | |
| # 実際の企業名 | |
| REAL_COMPANIES = [ | |
| "株式会社FLUX", "SmartHR", "カケハシ", "メルカリ", "Sansan", | |
| "freee", "BASE", "STORES", "oVice", "SmartNews", | |
| "note", "FOLIO", "WealthNavi", "NewsPicks", "リクルート" | |
| ] | |
| # サンプルデータ作成関数 | |
| def create_sample_data(): | |
| logger.info("Creating sample data") | |
| sample_data = [] | |
| for company in REAL_COMPANIES[:3]: # 最初の3社だけ使う | |
| for period in ["初期", "中期", "最近"]: | |
| # ダミーのベクトルを作成(1536次元) | |
| embedding = np.random.normal(0, 0.1, 1536).tolist() | |
| sample_data.append({ | |
| "company": company, | |
| "period": period, | |
| "embedding": embedding | |
| }) | |
| return pd.DataFrame(sample_data) | |
| # 埋め込みデータ読み込み処理 | |
| def load_data(): | |
| try: | |
| csv_path = "embedding_data.csv" | |
| logger.info(f"Loading embedding data from: {csv_path}") | |
| if os.path.exists(csv_path): | |
| # CSVを読み込む | |
| df = pd.read_csv(csv_path) | |
| logger.info(f"CSV loaded with {len(df)} rows") | |
| # 埋め込みデータの変換を試みる | |
| try: | |
| def safe_convert_embedding(x): | |
| if isinstance(x, (list, np.ndarray)): | |
| return np.array(x) | |
| elif isinstance(x, tuple): | |
| return np.array(list(x)) # タプルをリストに変換 | |
| elif isinstance(x, str): | |
| try: | |
| parsed = ast.literal_eval(x) | |
| if isinstance(parsed, (list, tuple)): | |
| return np.array(parsed) | |
| else: | |
| return np.zeros(1536) | |
| except: | |
| return np.zeros(1536) | |
| else: | |
| return np.zeros(1536) | |
| df["embedding"] = df["embedding"].apply(safe_convert_embedding) | |
| logger.info("Successfully converted embeddings") | |
| except Exception as e: | |
| logger.error(f"Error processing embeddings: {str(e)}") | |
| df = create_sample_data() | |
| else: | |
| logger.error(f"File not found: {csv_path}") | |
| df = create_sample_data() | |
| except Exception as e: | |
| logger.error(f"Error loading data: {str(e)}") | |
| df = create_sample_data() | |
| return df | |
| # API ロジック関数 | |
| def classify_values(inputs, api_key=OPENAI_API_KEY, model="gpt-3.5-turbo"): | |
| """入力された価値観をOpenAI APIを使って分類""" | |
| try: | |
| logger.info("Classifying values...") | |
| if not api_key or not api_key.startswith("sk-"): | |
| logger.error("Invalid API key format or no API key provided in environment") | |
| return """ | |
| 1. 挑戦心 | |
| 2. リモート可 | |
| 3. 多様性 | |
| 4. 研修充実 | |
| 5. 高報酬 | |
| """ | |
| client = OpenAI(api_key=api_key) | |
| prompt = f"以下の価値観を3〜5個に分類してください:\n{inputs}" | |
| logger.info(f"Sending request to OpenAI API with model: {model}") | |
| res = client.chat.completions.create( | |
| model=model, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=500, | |
| timeout=30 | |
| ) | |
| logger.info("Successfully received response from OpenAI") | |
| return res.choices[0].message.content | |
| except Exception as e: | |
| error_msg = str(e) | |
| logger.error(f"Error in classify_values: {error_msg}") | |
| # エラー時はダミーデータを返す | |
| return """ | |
| 1. 挑戦心 | |
| 2. リモート可 | |
| 3. 多様性 | |
| 4. 研修充実 | |
| 5. 高報酬 | |
| """ | |
| def create_dummy_results(): | |
| """ダミーの結果データを生成""" | |
| logger.info("Creating dummy results") | |
| return [ | |
| {"会社名": "株式会社FLUX", "初期": 72.3, "中期": 78.5, "最近": 85.7, | |
| "文化特性": {"革新性": 85, "安定性": 70, "成長機会": 90, "環境": 75, "報酬": 80}, | |
| "マッチング理由": "FLUXは挑戦心を重視する風土があり、あなたの成長意欲にぴったりです。リモートワークの柔軟な環境も整備されており、多様な働き方を実現できます。"}, | |
| {"会社名": "SmartHR", "初期": 68.1, "中期": 72.4, "最近": 79.8, | |
| "文化特性": {"革新性": 75, "安定性": 85, "成長機会": 70, "環境": 65, "報酬": 90}, | |
| "マッチング理由": "SmartHRは高い報酬水準と安定した企業基盤があり、長期的なキャリア形成に最適です。研修制度が充実しており、専門的なスキルアップを支援します。"}, | |
| {"会社名": "カケハシ", "初期": 65.7, "中期": 70.2, "最近": 76.4, | |
| "文化特性": {"革新性": 90, "安定性": 60, "成長機会": 85, "環境": 80, "報酬": 75}, | |
| "マッチング理由": "カケハシは革新的なアイデアを歓迎する社風で、あなたのクリエイティブな発想を活かせます。多様性を重視したチーム編成により、様々な視点から学べる環境です。"} | |
| ] | |
| def compute_top3(summary, df, api_key=OPENAI_API_KEY): | |
| """価値観の分類結果と埋め込みデータから最適な企業TOP3を計算""" | |
| try: | |
| logger.info("Computing top 3 matches...") | |
| if not api_key or not api_key.startswith("sk-"): | |
| logger.error("Invalid API key in compute_top3") | |
| return create_dummy_results() | |
| client = OpenAI(api_key=api_key) | |
| # サマリーテキストを行ごとに分割して処理 | |
| summary_lines = [line for line in summary.splitlines() if line.strip()] | |
| logger.info(f"Processing {len(summary_lines)} summary lines") | |
| user_vecs = [] | |
| for line in summary_lines: | |
| try: | |
| logger.info(f"Getting embedding for: {line[:30]}...") | |
| r = client.embeddings.create( | |
| input=line, | |
| model="text-embedding-ada-002", | |
| timeout=20 | |
| ) | |
| user_vecs.append(np.array(r.data[0].embedding)) | |
| except Exception as e: | |
| logger.error(f"Error getting embedding: {str(e)}") | |
| random_vec = np.random.uniform(-0.1, 0.1, 1536) | |
| random_vec = random_vec / np.linalg.norm(random_vec) | |
| user_vecs.append(random_vec) | |
| if not user_vecs: | |
| logger.error("No valid user vectors generated") | |
| return create_dummy_results() | |
| # 企業データの処理 | |
| companies = {} | |
| for _, r in df.iterrows(): | |
| try: | |
| c = r["company"] | |
| p = r["period"] | |
| if "embedding" in r: | |
| emb = r["embedding"] | |
| if not isinstance(emb, np.ndarray): | |
| if isinstance(emb, (list, tuple)): | |
| emb = np.array(emb) | |
| else: | |
| logger.error(f"Skipping invalid embedding for {c}, {p}: {type(emb)}") | |
| continue | |
| if len(emb.shape) != 1 or emb.shape[0] < 10: | |
| logger.error(f"Skipping invalid embedding dimensions for {c}, {p}: {emb.shape}") | |
| continue | |
| companies.setdefault(c, {})[p] = emb | |
| else: | |
| logger.error(f"No embedding found for {c}, {p}") | |
| except Exception as e: | |
| logger.error(f"Error processing company {r.get('company', 'unknown')}: {str(e)}") | |
| logger.info(f"Processed data for {len(companies)} companies") | |
| if not companies: | |
| logger.error("No valid companies found") | |
| return create_dummy_results() | |
| # 類似度計算とランキング | |
| results = [] | |
| for c, periods in companies.items(): | |
| if "最近" not in periods: | |
| logger.info(f"Skipping {c} - no recent data") | |
| continue | |
| def score(p): | |
| if p not in periods: | |
| return None | |
| try: | |
| sims = [] | |
| for uv in user_vecs: | |
| similarity = np.dot(periods[p], uv) / (np.linalg.norm(periods[p]) * np.linalg.norm(uv)) | |
| sims.append(similarity) | |
| return round(np.mean(sims) * 100, 1) | |
| except Exception as e: | |
| logger.error(f"Error calculating score for {c}, {p}: {str(e)}") | |
| return None | |
| company_result = { | |
| "会社名": c, | |
| "初期": score("初期"), | |
| "中期": score("中期"), | |
| "最近": score("最近"), | |
| "文化特性": { | |
| "革新性": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))), | |
| "安定性": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))), | |
| "成長機会": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))), | |
| "環境": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))), | |
| "報酬": min(100, max(0, int((score("最近") or 50) * (0.8 + 0.4 * np.random.random())))) | |
| } | |
| } | |
| results.append(company_result) | |
| sorted_results = sorted(results, key=lambda x: x["最近"] or 0, reverse=True) | |
| top3 = sorted_results[:3] | |
| if not top3: | |
| logger.error("No results found after sorting") | |
| return create_dummy_results() | |
| logger.info(f"Successfully computed top 3 matches: {[c['会社名'] for c in top3]}") | |
| # マッチング理由の生成 | |
| for company in top3: | |
| company["マッチング理由"] = generate_matching_reason(company, summary, api_key) | |
| return top3 | |
| except Exception as e: | |
| logger.error(f"Error in compute_top3: {str(e)}") | |
| return create_dummy_results() | |
| def generate_matching_reason(company, user_values, api_key=OPENAI_API_KEY, model="gpt-3.5-turbo"): | |
| """企業とユーザーの価値観のマッチング理由を生成""" | |
| try: | |
| if not api_key or not api_key.startswith("sk-"): | |
| # ダミーの理由を返す | |
| if company["会社名"] == "株式会社FLUX": | |
| return "FLUXは挑戦心を重視する風土があり、あなたの成長意欲にぴったりです。リモートワークの柔軟な環境も整備されており、多様な働き方を実現できます。" | |
| elif company["会社名"] == "SmartHR": | |
| return "SmartHRは高い報酬水準と安定した企業基盤があり、長期的なキャリア形成に最適です。研修制度が充実しており、専門的なスキルアップを支援します。" | |
| else: | |
| return "多様性を重視したチーム編成があり、様々な視点から学べる環境です。成長機会が豊富で、あなたのスキルアップを強力にサポートします。" | |
| client = OpenAI(api_key=api_key) | |
| # 企業の特性をテキストに整形 | |
| company_traits = [] | |
| for trait, value in company["文化特性"].items(): | |
| if value >= 80: | |
| company_traits.append(f"{trait}が非常に高い") | |
| elif value >= 70: | |
| company_traits.append(f"{trait}が高い") | |
| # プロンプト作成 | |
| prompt = f""" | |
| ユーザーの価値観: | |
| {user_values} | |
| 企業の特徴: | |
| 会社名: {company['会社名']} | |
| 特性: {', '.join(company_traits)} | |
| 上記のユーザーの価値観と企業の特徴を基に、この企業がユーザーの価値観にマッチする理由を2-3文で簡潔に説明してください。 | |
| 以下の条件を守ってください: | |
| - ポジティブな表現を使う | |
| - 抽象的すぎず具体的なポイントに言及する | |
| - 自然な日本語で、読みやすく具体的な表現にする | |
| - 理由は最大2つまでに絞り、簡潔に述べる | |
| """ | |
| logger.info(f"Generating matching reason for {company['会社名']}") | |
| res = client.chat.completions.create( | |
| model=model, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=200, | |
| temperature=0.7, | |
| timeout=20 | |
| ) | |
| # 応答から余分な空白や改行を削除 | |
| matching_reason = res.choices[0].message.content.strip() | |
| return matching_reason | |
| except Exception as e: | |
| logger.error(f"Error generating matching reason: {str(e)}") | |
| # エラー時のフォールバック理由 | |
| return f"{company['会社名']}は、あなたの価値観と高い相性を持っています。特に成長機会の豊富さとチームの多様性が、あなたのキャリア目標にマッチするでしょう。" | |
| def run_app(q1_choice, q1_text, q2_choice, q2_text, q3_choice, q3_text, | |
| q4_choice, q4_text, q5_choice, q5_text, progress=gr.Progress()): | |
| """リッチインタラクティブデザイン用の実行関数(エラーハンドリング強化版)""" | |
| try: | |
| logger.info("Starting app execution...") | |
| # APIキーチェック(環境変数から取得) | |
| api_key = OPENAI_API_KEY | |
| if not api_key: | |
| logger.warning("No API key found in environment variables") | |
| return "APIキーが設定されていません。管理者に連絡してください。", "", gr.update(visible=False), gr.update(value="次へ進む", variant="primary") | |
| # 回答データの収集 | |
| answers = [] | |
| for i, (choice, text) in enumerate([ | |
| (q1_choice, q1_text), (q2_choice, q2_text), (q3_choice, q3_text), | |
| (q4_choice, q4_text), (q5_choice, q5_text) | |
| ]): | |
| v = text.strip() or choice or None | |
| if v: | |
| answers.append(v) | |
| logger.info(f"Question {i+1}: {v[:30]}...") | |
| # 回答チェック | |
| if not answers: | |
| logger.warning("No answers provided") | |
| return "回答が入力されていません", "", gr.update(visible=True), gr.update(value="次へ進む", variant="primary") | |
| # 1. 価値観分類 | |
| progress(0.1, "価値観を分類中...") | |
| logger.info("Classifying values...") | |
| summary = classify_values(", ".join(answers), api_key) | |
| # 2. 企業マッチング計算 | |
| progress(0.4, "企業とのマッチングを計算中...") | |
| logger.info("Computing matches...") | |
| top3 = compute_top3(summary, df, api_key) | |
| # 結果HTML生成(シンプルな構造) | |
| summary_html = f'<div class="section"><h3>あなたの価値観</h3><div>{summary.replace(chr(10), "<br>")}</div></div>' | |
| # 企業カードHTML生成 - 横並びに変更 | |
| companies_html = '<div class="section"><h3>マッチング企業</h3><div class="company-grid">' | |
| for i, company in enumerate(top3): | |
| rank = i + 1 | |
| medal = "🥇" if rank == 1 else "🥈" if rank == 2 else "🥉" | |
| score = company["最近"] if company["最近"] is not None else 0 | |
| progress_width = min(int(score), 100) if score else 0 | |
| initial = f"{company['初期']}%" if company['初期'] is not None else "-" | |
| middle = f"{company['中期']}%" if company['中期'] is not None else "-" | |
| recent = f"{company['最近']}%" if company['最近'] is not None else "-" | |
| # マッチング理由 | |
| matching_reason = company.get("マッチング理由", "この企業はあなたの価値観と高い相性があります。") | |
| companies_html += f''' | |
| <div class="company-card"> | |
| <div class="company-header"> | |
| <span class="medal">{medal}</span> | |
| <span class="company-name">{company['会社名']}</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" style="width: {progress_width}%;"></div> | |
| </div> | |
| <div class="score">{recent}</div> | |
| <div class="periods"> | |
| <div class="period"> | |
| <div class="period-label">初期</div> | |
| <div class="period-value">{initial}</div> | |
| </div> | |
| <div class="period"> | |
| <div class="period-label">中期</div> | |
| <div class="period-value">{middle}</div> | |
| </div> | |
| <div class="period highlight"> | |
| <div class="period-label">最近</div> | |
| <div class="period-value">{recent}</div> | |
| </div> | |
| </div> | |
| <div class="matching-reason"> | |
| <h4>マッチング理由</h4> | |
| <p>{matching_reason}</p> | |
| </div> | |
| </div> | |
| ''' | |
| companies_html += '</div></div>' | |
| # CSS スタイリング - 横並び表示のため調整 | |
| style = ''' | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+JP:wght@300;400;500;700&display=swap'); | |
| .result-container { | |
| font-family: 'Inter', 'Noto Sans JP', sans-serif; | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| background: #f8fafc; | |
| border-radius: 16px; | |
| } | |
| .title { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| color: #3b82f6; | |
| } | |
| .section { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); | |
| } | |
| h3 { | |
| margin-top: 0; | |
| margin-bottom: 15px; | |
| color: #3b82f6; | |
| font-weight: 600; | |
| } | |
| .company-grid { | |
| display: flex; | |
| flex-direction: row; | |
| flex-wrap: nowrap; | |
| gap: 20px; | |
| overflow-x: auto; | |
| } | |
| .company-card { | |
| background: #f8fafc; | |
| border-radius: 8px; | |
| padding: 15px; | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | |
| flex: 1; | |
| min-width: 300px; | |
| max-width: 32%; | |
| } | |
| .company-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| } | |
| .medal { | |
| font-size: 24px; | |
| } | |
| .company-name { | |
| font-weight: 600; | |
| color: #3b82f6; | |
| } | |
| .progress-bar { | |
| height: 8px; | |
| background: #e2e8f0; | |
| border-radius: 4px; | |
| margin-bottom: 5px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: #3b82f6; | |
| border-radius: 4px; | |
| } | |
| .score { | |
| text-align: right; | |
| font-weight: 600; | |
| color: #3b82f6; | |
| margin-bottom: 15px; | |
| } | |
| .periods { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 15px; | |
| } | |
| .period { | |
| flex: 1; | |
| background: #f1f5f9; | |
| padding: 8px; | |
| border-radius: 6px; | |
| text-align: center; | |
| } | |
| .period.highlight { | |
| background: #dbeafe; | |
| } | |
| .period-label { | |
| font-size: 12px; | |
| color: #64748b; | |
| margin-bottom: 5px; | |
| } | |
| .period-value { | |
| font-weight: 500; | |
| } | |
| .highlight .period-value { | |
| color: #3b82f6; | |
| font-weight: 600; | |
| } | |
| .matching-reason { | |
| margin-top: 15px; | |
| padding: 12px; | |
| background: #fff; | |
| border-radius: 8px; | |
| border-left: 3px solid #3b82f6; | |
| } | |
| .matching-reason h4 { | |
| margin: 0 0 8px 0; | |
| color: #3b82f6; | |
| font-size: 14px; | |
| font-weight: 600; | |
| } | |
| .matching-reason p { | |
| margin: 0; | |
| color: #334155; | |
| font-size: 14px; | |
| line-height: 1.5; | |
| } | |
| /* モバイル対応 */ | |
| @media (max-width: 768px) { | |
| .company-grid { | |
| flex-wrap: wrap; | |
| overflow-x: visible; | |
| } | |
| .company-card { | |
| min-width: 100%; | |
| max-width: 100%; | |
| margin-bottom: 15px; | |
| } | |
| } | |
| </style> | |
| ''' | |
| # 完全なHTML結果を構築 | |
| results_html = f''' | |
| {style} | |
| <div class="result-container"> | |
| <div class="title"> | |
| <h2>企業文化マッチング結果</h2> | |
| </div> | |
| {summary_html} | |
| {companies_html} | |
| </div> | |
| ''' | |
| progress(1.0, "完了!") | |
| time.sleep(0.5) | |
| logger.info("App execution completed successfully") | |
| return summary, results_html, gr.update(visible=True), gr.update(value="分析完了 ✓", variant="secondary") | |
| except Exception as e: | |
| logger.error(f"Critical error in run_app: {str(e)}") | |
| logger.error(traceback.format_exc()) | |
| error_html = f''' | |
| <div style="text-align: center; padding: 40px; background: #fff5f5; border-radius: 12px; border: 1px solid #fed7d7;"> | |
| <div style="font-size: 48px; margin-bottom: 20px;">⚠️</div> | |
| <h3 style="margin-bottom: 15px; color: #e53e3e;">エラーが発生しました</h3> | |
| <p style="margin-bottom: 20px;">予期せぬエラーが発生しました。以下の対処法をお試しください:</p> | |
| <ul style="text-align: left; margin-bottom: 20px;"> | |
| <li>インターネット接続を確認してください</li> | |
| <li>ブラウザを更新してから再度お試しください</li> | |
| <li>しばらく時間をおいて再度お試しください</li> | |
| </ul> | |
| <p style="font-size: 14px; color: #718096;">エラー詳細: {str(e)[:100]}</p> | |
| </div> | |
| ''' | |
| return f"エラーが発生しました: {str(e)}", error_html, gr.update(visible=True), gr.update(value="再試行", variant="primary") | |
| # CSS定義(シンプル版) | |
| custom_css = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Noto+Sans+JP:wght@300;400;500;700&display=swap'); | |
| body { | |
| font-family: 'Inter', 'Noto Sans JP', sans-serif; | |
| color: #334155; | |
| background-color: #f8fafc; | |
| } | |
| .content-card { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 20px; | |
| margin-bottom: 15px; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); | |
| } | |
| .primary-button { | |
| background: #3b82f6 !important; | |
| color: white !important; | |
| font-weight: 600 !important; | |
| border-radius: 8px !important; | |
| padding: 12px 20px !important; | |
| } | |
| .secondary-button { | |
| background: #f1f5f9 !important; | |
| color: #64748b !important; | |
| font-weight: 600 !important; | |
| border-radius: 8px !important; | |
| padding: 12px 20px !important; | |
| } | |
| """ | |
| # Gradio Blocks 定義 - APIキー入力欄を削除 | |
| with gr.Blocks(css=custom_css, title="企業文化マッチング") as demo: | |
| # ヘッダー | |
| gr.HTML("<h1 style='text-align: center; margin: 30px 0 20px; color: #3b82f6;'>企業文化マッチング</h1>") | |
| gr.HTML("<p style='text-align: center; margin-bottom: 30px; color: #64748b;'>あなたの価値観に合った企業を見つけましょう</p>") | |
| # 質問1 | |
| with gr.Group(elem_classes="content-card"): | |
| gr.HTML("<h3 style='margin-top: 0;'>Q1: 仕事における優先事項</h3>") | |
| gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>あなたが仕事で最も大切にしている要素を選択してください</p>") | |
| q1_choice = gr.Radio( | |
| ["柔軟性", "挑戦心", "安定志向", "成長機会重視", "ワークライフバランス", "高給与"], | |
| label="最優先の価値観" | |
| ) | |
| q1_text = gr.Textbox( | |
| label="その他の重要な価値観", | |
| placeholder="例:チームでの協力、創造性を発揮できる環境" | |
| ) | |
| # 質問2 | |
| with gr.Group(elem_classes="content-card"): | |
| gr.HTML("<h3 style='margin-top: 0;'>Q2: 理想的な働き方</h3>") | |
| gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>あなたが重視する働き方や環境を選択してください</p>") | |
| q2_choice = gr.Radio( | |
| ["リモート可", "フレックス", "完全出社", "ハイブリッド勤務"], | |
| label="希望する働き方" | |
| ) | |
| q2_text = gr.Textbox( | |
| label="その他の希望", | |
| placeholder="例:集中できる個室環境、リラックスできるスペース" | |
| ) | |
| # 質問3 | |
| with gr.Group(elem_classes="content-card"): | |
| gr.HTML("<h3 style='margin-top: 0;'>Q3: チームや組織のあり方</h3>") | |
| gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>あなたが求める組織の特徴を選択してください</p>") | |
| q3_choice = gr.Radio( | |
| ["裁量権大", "多様性", "明確な役割分担", "リーダーシップ重視"], | |
| label="求める組織の特徴" | |
| ) | |
| q3_text = gr.Textbox( | |
| label="その他のポイント", | |
| placeholder="例:オープンなコミュニケーション" | |
| ) | |
| # 質問4 | |
| with gr.Group(elem_classes="content-card"): | |
| gr.HTML("<h3 style='margin-top: 0;'>Q4: 成長機会</h3>") | |
| gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>あなたが期待する成長機会を選択してください</p>") | |
| q4_choice = gr.Radio( | |
| ["研修充実", "メンター制度", "自己学習支援", "海外研修"], | |
| label="期待する成長機会" | |
| ) | |
| q4_text = gr.Textbox( | |
| label="その他の希望", | |
| placeholder="例:最新技術の習得機会" | |
| ) | |
| # 質問5 | |
| with gr.Group(elem_classes="content-card"): | |
| gr.HTML("<h3 style='margin-top: 0;'>Q5: その他の大切な要素</h3>") | |
| gr.HTML("<p style='color: #64748b; margin-bottom: 10px;'>その他にキャリアで重視することを選択してください</p>") | |
| q5_choice = gr.Radio( | |
| ["企業文化重視", "高報酬", "勤務地", "社会貢献度"], | |
| label="その他の重視ポイント" | |
| ) | |
| q5_text = gr.Textbox( | |
| label="具体的に", | |
| placeholder="例:副業可、社会的意義のある事業" | |
| ) |