0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

簡易的なアイデア生成アプリを作ってみた

0
Last updated at Posted at 2026-02-21

初めに

プログラミングで何を作りたいのか分からなくなるときがあるので、アイデアを出してくれるアプリをLLMとNode.jsで作ってみました。

この記事の内容を使用して起こった問題について一切の責任を負いかねます。ご了承ください

準備

まず、以下のコマンドを使用して、package.jsonを生成します:

bash
npm init -y

次に、以下のモジュールを以下のコマンドでインストールしてください:

bash
npm install express cors openai zod@3 zod-to-json-schema@3

※...Node.jsをインストールしていない場合はこちらからNode.jsの最新版をダウンロードしてください。

モジュールの説明

  • express: サーバーを動かすために使いました
  • cors: ブラウザからアクセスできるようにするために使いました
  • openai: llmに応答を生成させるために使いました
  • zod: LLMの構造化出力させるための雛形を作るときに使いました
  • zod-to-json-schema: zodで作ったものを、JSONスキーマに変換するために使いました

ファイルの配置:

今回は以下のようにファイルを作りました:

.
├── app.js
├── prompt.txt
├── public
│   └── index.html
└── settings.js

ファイルの説明:

  • app.js: メインのファイルです
  • settings.js: 設定ファイルです
  • prompt.txt: LLMのシステムプロンプトです
  • public/index.html: ウェブページです

コピペ:

以下のファイルの内容をコピペしてください

app.js:

app.js
// server file

// モジュールのインポート:
const express = require('express');
const cors = require('cors');
const path = require('node:path');
const fs = require('node:fs')
const OpenAI = require('openai');
const { zodToJsonSchema } = require('zod-to-json-schema')
const { z } = require('zod');
const settings = require('./settings');

// サーバーの初期化:
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));

// openaiの設定:
const client = new OpenAI({
    baseURL: settings.llm.base,
    apiKey: settings.llm.apikey
});

// その他の設定をロード:
const port = settings.server.port || 3000

// functions:
function Isstring(string) {
    return typeof string === 'string';
}

// 渡された文字列が安全かどうかをチェック(安全ならtrue, 危険ならfalseをreturn)
function check_safety(text) {
    if(!text || !Isstring(text) || text.trim() === '') return false;
    
    // whitelistのテスト形式
    const allowed = /[^一-龯ぁ-んァ-ヶa-zA-Z!??。、.,ー・()  ]/g;
    if(allowed.test(text)) {
        return false;
    };

    return true;
}

// 応答を生成
async function generate_response(input) {
    console.log('generating response. please wait...');

    if(!input || !check_safety(input)) return {status: 400, detail: 'missing or unexpecting: input'};

    // LLMの構造化出力をさせるための構造を作成(zodを使用):
    const params = z.object({
        title: z.string().describe('アイデア提案のタイトル。できるだけユーザーにわかりやすく'),
        description: z.string().describe('アイデアの詳細。使用言語などは書かずに、そのアイデアの仕様や特徴を詳しく説明する'),
        useprogramminglangs: z.array(z.string().describe('使用するプログラミング言語')).describe('そのアイデアに使用するプログラミング言語。できるだけユーザーが指定した言語に、指定がなければランダムでも可'),
        time_req: z.number().int().min(0).describe('作成にかかる所要時間(予測)。必ず分(minutes)で換算すること。(必ず0以上で)')
    });

    const { $schema, definitions, ...r_format } = zodToJsonSchema(params, {
         $refStrategy: "none",
         definitionPath: "none"
    });

    const sys_prompt = fs.readFileSync(path.join(__dirname, 'prompt.txt'), 'utf-8');
    const model = settings.llm.model;

    console.log('model: ' + model);
    console.log('temperature: ' + settings.llm.temperature);
    console.log('\nsystem:\n' + sys_prompt);
    console.log('\nuser:\n' + input);

    const response = await client.chat.completions.create({
        model: model,
        messages: [
            { role: 'system', content: sys_prompt},
            { role: 'user', content: input }
        ],
        response_format: {
            type: 'json_schema',
            json_schema: {
                name: 'idea_generate',
                strict: true,
                schema: {
                    ...r_format,
                    additionalProperties: false
                }
            }
        },
        temperature: settings.llm.temperature
    });

    console.log('...done');

    try {
        const parsed = JSON.parse(response.choices[0].message.content);
        return { status: 200, response: parsed };
    } catch (err) {
        return { status: 500, type: 'something went wrong', detail: err };
    }
}

// エンドポイント:
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

// LLMの応答を取得してクライアントに返す
app.post('/generate', async (req, res) => {

    // 渡された文字列が安全かどうかチェック
    const user_input = req.body.content;
    if(!check_safety(user_input)) {
        return res.status(400).json({exit_code: 400, err_type: 'bad_request', detail: 'invalid or unsafe prompt.'});
    };

    // LLMの応答を取得:
    const response = await generate_response(user_input);
    
    if(response.status !== 200) {
        return res.status(500).json({ err_type: 'internal_function_error', detail: response.detail});
    }

    // 応答
    try {
        res.status(200).json({ response: response, detail: 'success.'});
    } catch(err) {
        console.log(err);
        res.status(500).json({ err_type: 'something went wrong', detail: err});
    }
    })

// サーバーの起動
app.listen(port, () => {
    console.log(`server running on port ${port}.`);
})

settings.js:

settings.js
const path = require('node:path');

const settings = {
    server: {
        port: 3000
    },
    dir: {

    }, 
    llm: {
        base: 'http://localhost:11434/v1',
        apikey: 'dummy_key',
        model: 'enter modelname here.',
        temperature: 0.8
    }
}

module.exports = settings;

index.html:

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>アイデア生成装置</title>
    <style>
        body, html {
            padding: none;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
        }
        
        body {
            background: #212121;
        }

        .main {
            margin: 0 auto;
            text-align: center;
        }
        
        .main h1, 
        .main h2,
        .main h3,
        .main h4,
        .main h5,
        .main p {
            color: rgb(204, 204, 204);
        }

        .input_txt {
            background-color: black;
            color: rgb(204, 204, 204);
            resize: vertical;
            height: 300px;
            width: 600px;
        }

        .input_txt:hover {
            background: rgb(29, 29, 29);
        }

        .input_txt:disabled {
            cursor: not-allowed;
            background: rgb(85, 85, 85);
        }

        button {
            color: rgb(204, 204, 204);
            background-color: #181822;
            border-radius: 20px;
            border: none;
            height: 30px;
            width: 130px;
        }

        button:hover {
            background-color: #20202e;
            cursor: pointer;
        }

        button:active {
            background-color: #1e1e2b;
            cursor: pointer;
        }

        button:disabled {
            background-color: #303046;
            cursor: not-allowed;
        }

        .result {
            background: black;
            color: rgb(204, 204, 204);
            padding-left: 10px;
            padding: 10px 10px;
            width: 600px;
            border-radius: 10px;
            height: auto;
            margin: 0 auto;
        }

        .result h1,
        .result h2,
        .result h3,
        .result h4,
        .result h5,
        .result p {
            font-weight: 200;
        }
    </style>
</head>
<body>
    <div class="main">
        <h1>アイデア生成装置</h1>
        <p>アイデアが思いつかないときに使ってください。</p>
        <br><p>ここに生成してほしい内容を入力:</p>
        <p id="navi"></p>
        <p id="navi2"></p>
        <textarea id="prompt" class="input_txt" title="ここに生成してほしい内容を入力"></textarea>
        <br><br><button id="generate" title="応答を生成">生成</button>
        
    </div>
    <div id="result" hidden>
        <br><h2 style="text-align: center; color: rgb(204, 204, 204);">生成結果:</h2>
        <div class="result">
        <span><h2 id="res-title">タイトル:これはテストです</h2></span>
        <h3>詳細:</h3>
        <p id="res-description">テスト用のやつです。</p>
        <h3>使用する言語:</h3>
        <p id="res-langs">Javascript</p>
        <h3>所要時間:</h3>
        <p id="res-time_req">60分</p>
    </div>
    </div>
</body>
<script>
    const navi = document.getElementById('navi');
    const navi2 = document.getElementById('navi2');
    const btn = document.getElementById('generate');
    const input = document.getElementById('prompt');

    function show_result(data) {
        if(!data.title || !data.description || !data.useprogramminglangs || !data.time_req) {
             navi2.style = 'color: red';
             navi2.textContent = 'error: missing data.'
             return;
        }

        document.getElementById('result').hidden = false;

        document.getElementById('res-title').textContent = 'タイトル:' + data.title;
        document.getElementById('res-description').textContent = data.description;
        document.getElementById('res-langs').textContent = data.useprogramminglangs;
        document.getElementById('res-time_req').textContent = data.time_req + '分';

    }

    btn.addEventListener('click', async () => {

        btn.disabled = true;
        input.disabled = true;
        navi.textContent = '';
        navi2.textContent = '';
        const data = input.value;
        const Promptdata = data.replace(/[^一-龯ぁ-んァ-ヶa-zA-Z!??。、.,ー・()  ]/g, "");        
        
        if(!Promptdata) {
            navi2.style = 'color: red;'
            navi2.textContent = 'エラー:生成してほしい内容は必須です。';
            btn.disabled = false;
            input.disabled = false;
            return;
        };

        navi.style = 'color: white;'
        navi.textContent = 'generating idea. please wait...'
        
        if(Promptdata !== data) {
            navi2.style = 'color: red;'
            navi2.textContent = '一部の文字が危険と判断されて除外されました。実際に送信されたデータ:' + Promptdata
        }

        const resp = await fetch('/generate', {
            method: 'POST',
            headers: {
                'content-type': 'application/json'
            },
            body: JSON.stringify({
                content: Promptdata
            })
        });

        const response = await resp.json()
        
        if(resp.status !== 200) {
            navi2.style = 'color: red;';
            navi2.textContent = response.detail;
            btn.disabled = false;
            input.disabled = false;
            return;
        };

        console.log(response)
        navi.textContent = '';
        navi2.textContent = '';

        show_result(response.response.response);

        btn.disabled = false;
        input.disabled = false;
        return;
    })
</script>
</html>

prompt.txt

prompt.txt
あなたはユーザーにアイデアを出すAIアシスタントです。以下の制約や現在与えられている項目ごとの内容にそって生成してください

1. title...アイデアの名前
2. description...そのアイデアの概要です(どういうものなのか、何をするものなのか、それを作る難易度などを書いてください)
3. useprogramminglangs...そのアイデアで使用するプログラミング言語。ユーザーの指示に従うこと。しかし、指定がなければこの限りではない
4. time_req...そのアイデア(プログラミング)を完成させるために必要な時間。分(minutes)で表記すること

制約事項:
マークダウン記法(# や *、- など)は一切使用しないでください。
セクションの区切りや箇条書きは、全角の数字や記号のみを使って表現してください。
回答はすべて日本語で行ってください。

使い方:

settings.jsにアクセスして、model(デフォルトではenter modelname hereという値になっている部分です)を実際に存在するモデル名に変更してください:

app.jsがあるフォルダで以下のコマンドを実行します:

bash
node app

そしたら

server running on port 3000

みたいなメッセージが出てくるので、ブラウザでhttp://localhost:3000にアクセスします。

アクセスしたら以下のような画面になると思うので、生成したい内容を入力して生成ボタンを押します:
nQVaPK0F7MafJhT1771595008_1771595066.png

しばらくしたら生成結果が返ってきます(ちなみに自分はqwen3:4bを使って4分くらいで結果が返ってきました):

4y9g68eIcyf4GIe1771595235_1771595396.png

最後に

今回は簡易的なアイデア生成装置を作ってみました。ちなみに今回はHTMLとCSSは自分で書いて、LLMに与えるプロンプトはGeminiに考えてもらいました。それではまたお会いしましょう!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?