AI スクリプトエディター

AIスクリプトヘルプエディターって、どんなもの?

AIスクリプトヘルプエディターは、一言でいうと**「AIに指示を出すのが、もっとラクに、もっと楽しくなるツール」**です!

普段、AIに何かお願いするときって、文章を考えたり、何度も同じことを入力したり、ちょっと手間だと感じたことはありませんか? このエディターは、そんな「めんどくさい」を解消して、あなたがAIともっとスムーズにスクリプト作成できるようにサポートしてくれます。

AIって、私たちの指示次第でいろんなすごいことをしてくれますよね。でも、「どう指示したら一番いいの?」って、結構悩みがちだったりします。このエディターは、そんなあなたの「もっとこうだったらいいのに!」に応えるために生まれました。

  • 「また同じこと書くの?」をなくす!: AIに「こういう感じで書いてね」とか「この形式で出力してね」って、繰り返し伝えることってありますよね。そういうよく使うフレーズを、このエディターが覚えてくれるんです。もう何度も入力する手間はいりません。
  • AIとの会話をスムーズに!: もしあなたがチームでAIを使っているなら、「あの人、どんな指示出してるんだろう?」って思ったことありませんか?このエディターがあれば、みんなで使える「指示の型」(テンプレート)を作れるので、AIからの返事も安定して、もっと効率的に作業を進められますよ。
  • あなたの時間を大切に!: 毎回イチから文章を考えるのって、実はすごく時間を使ってるんです。このエディターが代わりに「型」を用意してくれるので、あなたは本当に伝えたいことに集中できます。その分、他の大事な作業に時間を使えるようになりますよ。

つまり、このエディターは、あなたがAIとの「お仕事」をもっと快適に、そしてスマートに進めるための頼れる相棒なんです!

  • wibndows版実行ファイルを任意の場所に解凍
  • _AIScriptHelperEditor.exeを実行 (Pythonは無くてもOK)

ブラウザ版 ブラウザで表示

ブラウザ版 解説

ブラウザ版(HTML/CSS/JavaScript):

設定(例: 最後に使ったテンプレート、Webリンクのリスト)は、ブラウザのLocal Storageなどの機能を使ってブラウザ内に一時的に保存することは可能ですが、ユーザーがブラウザのキャッシュをクリアすると消えてしまいます。settings.ini のような外部ファイルを読み書きすることはできません。

動作環境: Webブラウザ上で動作します。インターネット接続があれば、どのOSのどのデバイスからでもアクセス可能です(ブラウザの互換性に依存)。

ファイルアクセス: セキュリティ上の制約により、ローカルPCのファイルシステムへの直接アクセスは非常に限定的です。

テンプレートや定型文は、アプリケーションをホストしているサーバーから読み込むか、JavaScript内で直接定義されている必要があります。

編集した内容は、ユーザーが「ファイルに保存」ボタンをクリックすることで、ブラウザのダウンロード機能を通じて手動でダウンロードさせる形になります。直接指定のフォルダに自動保存することはできません。

Python版 解説

AIスクリプトヘルプエディター:ZIPからの起動と使い方
ダウンロードしたZIPファイルから、AIスクリプトヘルプエディターを素早く使うための手順です。
スクリプト版はPythonで動作します。
お使いのPCにPythonがインストールされている必要があります。

1. ZIPファイルを解凍
ダウンロードしたZIPファイルを右クリック(Macならダブルクリック)して解凍してください。中身のファイル(例: _editor_app_v5.1.pysettings.inicode_template.txt など)はすべて同じフォルダに置いておきましょう。

2. エディターを起動

解凍したフォルダ内の _editor_app_v5.1.py を実行します。

  • Windows: ファイルをダブルクリック
  • Mac/Linux: ターミナルでフォルダへ移動し、python _editor_app_v5.1.py を実行。

3. 基本的な使い方

「日付ファイルで保存」: 日付とテンプレート名入りのファイルで保存でき、履歴管理が楽になります。

左側: 定型指示リスト。ダブルクリックで右のエディターに追記。

右側: メインエディター。AIへの指示を記述。

上部メニュー:

「ファイル」: 新規作成、開く、保存など。

「テンプレート選択」: AIの目的に合わせたテンプレートを切り替え(内容と定型文が連動)。

スクリプト

import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext, Toplevel, StringVar, BooleanVar
import os
import subprocess
import sys
import configparser
import datetime
import webbrowser
import platform

# --- Constants ---
SETTINGS_FILE = "settings.ini"
DEFAULT_SETTINGS_CONTENT = """
[Settings]
last_template = 文書校正
# last_file_path =
# last_scri_file =

[Templates]
コード作成 = code_template.txt,_SCRI_code.txt
画像作成 = image_template.txt,_SCRI_image.txt
文書校正 = document_template.txt,_SCRI_document.txt
調査調査 = research_template.txt,_SCRI_research.txt

[DateSaveOptions]
save_to_my_documents_default = False
include_time_default = False
menu_label_script_folder_date_only = スクリプトフォルダに保存 (YYYYMMDD_テンプレ名.txt)
menu_label_script_folder_date_time = スクリプトフォルダに保存 (YYYYMMDD_HHMM_テンプレ名.txt)
menu_label_my_documents_date_only = マイドキュメントに保存 (YYYYMMDD_テンプレ名.txt)
menu_label_my_documents_date_time = マイドキュメントに保存 (YYYYMMDD_HHMM_テンプレ名.txt)

[WebLinks]
チャットgpt = https://chat.openai.com/
コパイロット = https://copilot.microsoft.com/
ジェミニ = https://gemini.google.com/
"""

class ScriptHelperEditor:
    """
    AIへのスクリプト作成を補助する機能を備えたテキストエディタ。
    エディタ枠外に行数表示機能、テンプレート選択・記憶機能を追加。
    """

    def __init__(self, root):
        self.root = root
        self.current_file_path = None # 現在開いているファイルのパス
        self.current_template = None # 現在選択中のテンプレート名
        self.current_scri_file = None # 現在のテンプレートに紐づく定型文ファイルのパス
        self.config = configparser.ConfigParser()

        # Search/Replace related variables (実装は省略されている可能性がありますが、変数定義は維持)
        self.search_toplevel = None
        self.replace_toplevel = None
        self.find_start_index = "1.0"
        self.last_search_term = None
        self.case_sensitive = BooleanVar(value=False)

        # Load or create settings.ini
        self._load_or_create_settings()

        # Initialize template and scri file mappings from config
        self.TEMPLATE_MAP = {}
        self.SCRI_TEMPLATE_MAP = {}
        if self.config.has_section('Templates'):
            for name, paths in self.config.items('Templates'):
                try:
                    editor_file, scri_file = paths.split(',')
                    self.TEMPLATE_MAP[name.strip()] = editor_file.strip()
                    self.SCRI_TEMPLATE_MAP[name.strip()] = scri_file.strip()
                except ValueError:
                    messagebox.showwarning("設定エラー", f"settings.iniの[Templates]セクションの設定が不正です: {name} = {paths}")
                    continue

        # Default date save options and menu labels
        self.save_to_my_documents_default = self.config.getboolean('DateSaveOptions', 'save_to_my_documents_default', fallback=False)
        self.include_time_default = self.config.getboolean('DateSaveOptions', 'include_time_default', fallback=False)
        self.menu_label_script_folder_date_only = self.config.get('DateSaveOptions', 'menu_label_script_folder_date_only', fallback="スクリプトフォルダに保存 (YYYYMMDD_テンプレ名.txt)")
        self.menu_label_script_folder_date_time = self.config.get('DateSaveOptions', 'menu_label_script_folder_date_time', fallback="スクリプトフォルダに保存 (YYYYMMDD_HHMM_テンプレ名.txt)")
        self.menu_label_my_documents_date_only = self.config.get('DateSaveOptions', 'menu_label_my_documents_date_only', fallback="マイドキュメントに保存 (YYYYMMDD_テンプレ名.txt)")
        self.menu_label_my_documents_date_time = self.config.get('DateSaveOptions', 'menu_label_my_documents_date_time', fallback="マイドキュメントに保存 (YYYYMMDD_HHMM_テンプレ名.txt)")

        # Load WebLinks
        self.web_links = {}
        if self.config.has_section('WebLinks'):
            for name, url in self.config.items('WebLinks'):
                self.web_links[name.strip()] = url.strip()

        self.root.protocol("WM_DELETE_WINDOW", self.exit_editor)

        self.root.title("AI Script Helper Editor")
        self.root.geometry("1100x750")

        # Main frame
        main_frame = tk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # Left frame (Fixed phrases area)
        left_frame = tk.Frame(main_frame, width=350)
        left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
        left_frame.pack_propagate(False)

        # Right frame (Text editor area)
        right_frame = tk.Frame(main_frame)
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)

        # --- Left: Fixed Phrases Display Area ---
        self.scri_label_text = tk.StringVar() # To dynamically update label text
        self.scri_label = tk.Label(left_frame, textvariable=self.scri_label_text, font=("Helvetica", 12, "bold"))
        self.scri_label.pack(pady=(0, 5), anchor="w")

        info_label = tk.Label(left_frame, text="ダブルクリックでエディタに追記", fg="blue")
        info_label.pack(pady=(0, 10), anchor="w")

        list_frame = tk.Frame(left_frame)
        list_frame.pack(fill=tk.BOTH, expand=True)

        self.scri_listbox = tk.Listbox(list_frame, font=("Meiryo UI", 10))
        self.scri_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar = tk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.scri_listbox.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.scri_listbox.config(yscrollcommand=scrollbar.set)

        self.scri_listbox.bind("<Double-1>", self.add_scri_to_editor)
        self.scri_listbox.bind("<Button-3>", self.show_scri_context_menu) # Right-click for listbox

        # 定型文リストボックスの操作ボタン
        scri_buttons_frame = tk.Frame(left_frame)
        scri_buttons_frame.pack(fill=tk.X, pady=(5, 0))

        move_up_button = tk.Button(scri_buttons_frame, text="▲ 上へ", command=lambda: self.move_scri_item(-1))
        move_up_button.pack(side=tk.LEFT, expand=True, padx=(0, 2))

        move_down_button = tk.Button(scri_buttons_frame, text="▼ 下へ", command=lambda: self.move_scri_item(1))
        move_down_button.pack(side=tk.LEFT, expand=True, padx=(2, 0))


        # --- Right: Text Editor and Related Widgets ---
        # Add line to fixed phrases feature
        add_scri_frame = tk.Frame(right_frame)
        add_scri_frame.pack(fill=tk.X, pady=(0, 5))

        self.line_num_entry = tk.Entry(add_scri_frame, width=10)
        self.line_num_entry.pack(side=tk.LEFT, padx=(0, 5))

        add_button = tk.Button(add_scri_frame, text="選択テンプレートの定型文に追記", command=self.add_line_to_scri)
        add_button.pack(side=tk.LEFT)

        # Spacer for 3 characters (approximate)
        tk.Label(add_scri_frame, text="   ").pack(side=tk.LEFT)

        # Web Links Buttons (settings.iniから読み込まれたリンクのみを生成)
        for name, url in self.web_links.items():
            link_button = tk.Button(add_scri_frame, text=name, command=lambda u=url: webbrowser.open_new(u))
            link_button.pack(side=tk.LEFT, padx=(2, 0)) # Add a small padding between buttons

        # !!!! ここに静的に "WEB表示" ボタンが追加されている場合は、その行を削除してください !!!!
        # 例: tk.Button(add_scri_frame, text="WEB表示", command=some_function).pack(side=tk.LEFT)
        # そのような行がなければ、このコメントは無視してください。

        # Clipboard copy button (これは残す)
        copy_editor_content_button = tk.Button(add_scri_frame, text="エディタ内容をコピー", command=self.copy_editor_content_to_clipboard)
        copy_editor_content_button.pack(side=tk.RIGHT, padx=(10, 0))


        # Text editor container frame
        editor_container_frame = tk.Frame(right_frame)
        editor_container_frame.pack(expand=True, fill="both")

        # Line numbers widget
        self.line_numbers = tk.Text(editor_container_frame, width=4, padx=3, takefocus=0,
                                     border=0, background="#f0f0f0", state='disabled',
                                     font=("Meiryo UI", 11))
        self.line_numbers.pack(side=tk.LEFT, fill=tk.Y)

        # Text editor itself
        self.text_editor = scrolledtext.ScrolledText(editor_container_frame, wrap=tk.WORD, undo=True, font=("Meiryo UI", 11))
        self.text_editor.pack(expand=True, fill="both", side=tk.LEFT)
        self.text_editor.bind("<Button-3>", self.show_editor_context_menu) # Right-click for editor

        # Configure tags for search highlighting
        self.text_editor.tag_configure("search", background="yellow")

        # Synchronize line numbers and text editor scrolling
        self.text_editor.vbar.config(command=self.yview_both)
        self.line_numbers.config(yscrollcommand=self.text_editor.yview)
        self.text_editor.config(yscrollcommand=self.yview_editor)

        # Bind text editor content change events
        self.text_editor.bind("<KeyRelease>", self._on_text_change)
        self.text_editor.bind("<ButtonRelease-1>", self._on_text_change)
        self.text_editor.bind("<MouseWheel>", self._on_text_change)
        self.text_editor.bind("<<Undo>>", self._on_text_change)
        self.text_editor.bind("<<Redo>>", self._on_text_change)
        self.text_editor.bind("<Control-f>", self.find_text)
        self.text_editor.bind("<Control-h>", self.replace_text)


        # --- Create Menu Bar ---
        self.create_menu()

        # --- Footer for Copyright and Link ---
        footer_frame = tk.Frame(self.root, bd=1, relief=tk.RAISED)
        footer_frame.pack(side=tk.BOTTOM, fill=tk.X)

        copyright_label = tk.Label(footer_frame, text="© sakaida.jp", font=("Arial", 9))
        copyright_label.pack(side=tk.LEFT, padx=5, pady=2)

        # Make the link clickable (これは残す: WEB表示ボタンとは別機能)
        link_label = tk.Label(footer_frame, text="sakaida.jp", fg="blue", cursor="hand2", font=("Arial", 9, "underline"))
        link_label.pack(side=tk.RIGHT, padx=5, pady=2)
        link_label.bind("<Button-1>", lambda e: webbrowser.open_new("https://sakaida.jp"))

        # --- Startup processing ---
        self.load_state_from_settings() # Load settings for last session

    def _load_or_create_settings(self):
        """Loads settings from settings.ini or creates it if it doesn't exist."""
        if not os.path.exists(SETTINGS_FILE):
            try:
                with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
                    f.write(DEFAULT_SETTINGS_CONTENT.strip())
                messagebox.showinfo("設定ファイル作成", f"'{SETTINGS_FILE}' が見つからなかったため、デフォルトの設定ファイルを作成しました。")
            except Exception as e:
                messagebox.showerror("エラー", f"設定ファイルの作成に失敗しました:\n{e}")
                sys.exit(1) # Critical error, exit application

        try:
            self.config.read(SETTINGS_FILE, encoding='utf-8')
        except Exception as e:
            messagebox.showerror("設定ファイル読み込みエラー", f"'{SETTINGS_FILE}' の読み込み中にエラーが発生しました:\n{e}\n\n"
                                 "ファイルが破損している可能性があります。削除して再起動してください。")
            sys.exit(1)


    def create_menu(self):
        """Creates the menu bar."""
        menubar = tk.Menu(self.root)
        self.root.config(menu=menubar)

        # ファイルメニュー
        file_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="ファイル", menu=file_menu)
        file_menu.add_command(label="新規作成", command=self.new_file)
        file_menu.add_command(label="開く", command=self.open_file)
        file_menu.add_command(label="上書き保存", command=self.save_file)
        file_menu.add_command(label="名前を付けて保存", command=self.save_as_file)
        file_menu.add_separator()
        file_menu.add_command(label="終了", command=self.exit_editor)

        # 編集メニュー
        edit_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="編集", menu=edit_menu)
        edit_menu.add_command(label="元に戻す", command=self.text_editor.edit_undo)
        edit_menu.add_command(label="やり直し", command=self.text_editor.edit_redo)
        edit_menu.add_separator()
        edit_menu.add_command(label="切り取り", command=lambda: self.text_editor.event_generate("<<Cut>>"))
        edit_menu.add_command(label="コピー", command=lambda: self.text_editor.event_generate("<<Copy>>"))
        edit_menu.add_command(label="貼り付け", command=lambda: self.text_editor.event_generate("<<Paste>>"))
        edit_menu.add_separator()
        edit_menu.add_command(label="すべて選択", command=lambda: self.text_editor.tag_add("sel", "1.0", "end"))
        edit_menu.add_separator()
        edit_menu.add_command(label="検索 (Ctrl+f)", command=self.find_text)
        edit_menu.add_command(label="置き換え (Ctrl+h)", command=self.replace_text)


        # テンプレート選択メニュー (Dynamically generated)
        template_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="テンプレート選択", menu=template_menu)
        for template_name in self.TEMPLATE_MAP.keys():
            template_menu.add_command(label=template_name, command=lambda name=template_name: self.set_template(name))

        # 日付ファイルで保存メニュー
        date_save_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="日付ファイルで保存", menu=date_save_menu)

        # スクリプトと同じフォルダに保存
        date_save_menu.add_command(
            label=self.menu_label_script_folder_date_only,
            command=lambda: self.save_file_with_date_and_template(include_time=False, to_my_documents=False)
        )
        date_save_menu.add_command(
            label=self.menu_label_script_folder_date_time,
            command=lambda: self.save_file_with_date_and_template(include_time=True, to_my_documents=False)
        )
        date_save_menu.add_separator()

        # マイドキュメントフォルダに保存
        date_save_menu.add_command(
            label=self.menu_label_my_documents_date_only,
            command=lambda: self.save_file_with_date_and_template(include_time=False, to_my_documents=True)
        )
        date_save_menu.add_command(
            label=self.menu_label_my_documents_date_time,
            command=lambda: self.save_file_with_date_and_template(include_time=True, to_my_documents=True)
        )

        # ヘルプメニューを追加
        help_menu = tk.Menu(menubar, tearoff=0)
        menubar.add_cascade(label="ヘルプ", menu=help_menu)
        help_menu.add_command(label="このエディターについて", command=self.open_about_editor_link)


    def open_about_editor_link(self):
        """このエディターについてのリンクを開く"""
        webbrowser.open_new("https://sakaida.jp/ai-sc-edit/")


    def show_editor_context_menu(self, event):
        """エディタの右クリックメニューを表示する"""
        context_menu = tk.Menu(self.root, tearoff=0)

        try:
            selected_text = self.text_editor.get(tk.SEL_FIRST, tk.SEL_LAST)
        except tk.TclError:
            selected_text = "" # No text selected

        if selected_text:
            context_menu.add_command(label="選択範囲を定型文に追加", command=self.add_selected_text_to_scri)
        else:
            context_menu.add_command(label="選択範囲を定型文に追加", state=tk.DISABLED) # Disable if no selection

        context_menu.add_separator()
        context_menu.add_command(label="切り取り", command=lambda: self.text_editor.event_generate("<<Cut>>"))
        context_menu.add_command(label="コピー", command=lambda: self.text_editor.event_generate("<<Copy>>"))
        context_menu.add_command(label="貼り付け", command=lambda: self.text_editor.event_generate("<<Paste>>"))
        context_menu.add_separator()
        context_menu.add_command(label="検索", command=self.find_text)
        context_menu.add_command(label="置き換え", command=self.replace_text)

        context_menu.tk_popup(event.x_root, event.y_root)

    def show_scri_context_menu(self, event):
        """定型文リストボックスの右クリックメニューを表示する"""
        context_menu = tk.Menu(self.root, tearoff=0)

        # 右クリックされた位置のアイテムを特定
        index = self.scri_listbox.nearest(event.y)
        if index != -1:
            self.scri_listbox.selection_clear(0, tk.END)
            self.scri_listbox.selection_set(index)
            self.scri_listbox.activate(index)

            selected_text = self.scri_listbox.get(index)

            context_menu.add_command(label="エディターへ追加", command=lambda: self.add_scri_to_editor(event))
            context_menu.add_command(label="選択肢から削除", command=lambda: self.delete_scri_item(selected_text))
            context_menu.add_separator()
            context_menu.add_command(label="上に移動", command=lambda: self.move_scri_item(-1))
            context_menu.add_command(label="下に移動", command=lambda: self.move_scri_item(1))
        else:
            context_menu.add_command(label="エディターへ追加", state=tk.DISABLED)
            context_menu.add_command(label="選択肢から削除", state=tk.DISABLED)
            context_menu.add_separator()
            context_menu.add_command(label="上に移動", state=tk.DISABLED)
            context_menu.add_command(label="下に移動", state=tk.DISABLED)

        context_menu.tk_popup(event.x_root, event.y_root)

    def _update_line_numbers(self, *args):
        """Updates and displays line numbers for the text editor."""
        self.line_numbers.config(state='normal')
        self.line_numbers.delete("1.0", tk.END)

        lines = self.text_editor.get("1.0", tk.END).split('\n')

        line_count = len(lines)
        line_numbers_text = ""
        for i in range(1, line_count + 1):
            line_numbers_text += str(i) + "\n"

        self.line_numbers.insert("1.0", line_numbers_text)
        self.line_numbers.config(state='disabled')

        self.line_numbers.yview_moveto(self.text_editor.yview()[0])

    def yview_both(self, *args):
        """Scrolls both the text editor and line number display."""
        self.text_editor.yview(*args)
        self.line_numbers.yview(*args)

    def yview_editor(self, *args):
        """Command for text editor scrollbar to also scroll line numbers."""
        self.text_editor.yview(*args)
        self.line_numbers.yview(*args)

    def _on_text_change(self, event=None):
        """Called when the content of the text editor changes."""
        # Clear search highlights on any text modification
        self.text_editor.tag_remove("search", "1.0", tk.END)

        if event and event.type == "23" and (event.keysym == "Undo" or event.keysym == "Redo"):
            self._update_line_numbers()
        elif event and event.keysym in ("BackSpace", "Delete", "Return"):
            self._update_line_numbers()
        elif event and event.char and len(event.char) == 1:
            self._update_line_numbers()
        elif event and (event.num == 1): # Mouse click
            self.root.after(10, self._update_line_numbers)
        elif event and (event.num == 4 or event.num == 5): # Mouse scroll
            self.line_numbers.yview_moveto(self.text_editor.yview()[0])

    def update_scri_label(self):
        """Updates the label for the fixed phrase listbox."""
        if self.current_template:
            scri_filename = os.path.basename(self.SCRI_TEMPLATE_MAP.get(self.current_template, "ファイルなし"))
            self.scri_label_text.set(f"定型指示 ({self.current_template} - {scri_filename})")
        else:
            if self.current_scri_file:
                self.scri_label_text.set(f"定型指示 ({os.path.basename(self.current_scri_file)})")
            else:
                self.scri_label_text.set("定型指示 (選択なし)")

    def load_scri_file(self):
        """Loads the current fixed phrase file and displays it in the listbox."""
        self.scri_listbox.delete(0, tk.END)
        self.update_scri_label()

        if not self.current_scri_file:
            return

        scri_filepath = self.current_scri_file
        # if the current_scri_file is just a filename (e.g., from SCRI_TEMPLATE_MAP),
        # assume it's in the script's directory.
        if not os.path.isabs(scri_filepath):
            scri_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_filepath)

        try:
            if not os.path.exists(scri_filepath):
                open(scri_filepath, 'w', encoding='utf-8').close()

            with open(scri_filepath, 'r', encoding='utf-8') as f:
                for line in f:
                    if line.strip():
                        self.scri_listbox.insert(tk.END, line.strip())
        except Exception as e:
            messagebox.showerror("エラー", f"定型文ファイル '{os.path.basename(scri_filepath)}' の読み込みに失敗しました:\n{e}")

    def append_text_to_current_scri_file(self, text_content):
        """汎用的にテキストを現在の定型文ファイルに追記する"""
        if not self.current_scri_file:
            messagebox.showwarning("警告", "定型文を追加するファイルが特定できません。")
            return False

        if not text_content.strip():
            # messagebox.showinfo("情報", "追加する内容が空です。") # このメッセージは表示しない
            return False

        scri_filepath = self.current_scri_file
        if not os.path.isabs(scri_filepath):
            scri_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_filepath)

        try:
            # 追記ではなく、リストボックスから直接ファイルに保存する形に変更
            # appendは行わない。一旦全てリストボックスに読み込み、リストボックスの変更をファイルに反映させる
            # ここでは単にリストボックスに要素を追加するのみ
            self.scri_listbox.insert(tk.END, text_content.strip())
            # messagebox.showinfo("成功", f"'{text_content[:30].strip()}...' を定型文リストに追加しました。") # このメッセージは表示しない
            self.save_scri_file_from_listbox() # リストボックスの内容をファイルに保存
            return True
        except Exception as e:
            messagebox.showerror("エラー", f"定型文リストへの追加に失敗しました:\n{e}")
            return False

    def add_line_to_scri(self):
        """エディタの指定行を現在の定型文ファイルに追記する"""
        try:
            line_num_str = self.line_num_entry.get()
            if not line_num_str.isdigit():
                messagebox.showwarning("入力エラー", "有効な行番号を数字で入力してください。")
                return

            line_num = int(line_num_str)
            line_content = self.text_editor.get(f"{line_num}.0", f"{line_num}.end").strip()

            self.append_text_to_current_scri_file(line_content)
            self.line_num_entry.delete(0, tk.END)

        except tk.TclError:
            messagebox.showerror("エラー", "指定された行番号は存在しません。")
        except Exception as e:
            messagebox.showerror("エラー", f"行の取得に失敗しました:\n{e}")

    def add_selected_text_to_scri(self):
        """エディタで選択されたテキストを現在の定型文ファイルに追記する"""
        try:
            selected_text = self.text_editor.get(tk.SEL_FIRST, tk.SEL_LAST)
            self.append_text_to_current_scri_file(selected_text)
        except tk.TclError:
            messagebox.showinfo("情報", "選択されているテキストがありません。")
        except Exception as e:
            messagebox.showerror("エラー", f"選択テキストの取得に失敗しました:\n{e}")


    def add_scri_to_editor(self, event):
        """リストボックスで選択した項目をテキストエディタに追記する (ダブルクリック/右クリック用)"""
        selected_indices = self.scri_listbox.curselection()
        if not selected_indices:
            return

        selected_text = self.scri_listbox.get(selected_indices[0])

        self.text_editor.insert(tk.INSERT, selected_text + "\n")

        original_bg = self.scri_listbox.itemcget(selected_indices[0], "background")
        self.scri_listbox.itemconfig(selected_indices[0], {'bg':'#aaddff'})
        self.root.after(200, lambda: self.scri_listbox.itemconfig(selected_indices[0], {'bg': original_bg}))

        self._update_line_numbers()

    def delete_scri_item(self, item_text):
        """定型文リストボックスから項目を削除する"""
        if not self.current_scri_file:
            messagebox.showwarning("警告", "削除対象の定型文ファイルが特定できません。")
            return

        selected_indices = self.scri_listbox.curselection()
        if not selected_indices:
            messagebox.showinfo("情報", "削除する項目を選択してください。")
            return

        # 念のため、選択されている項目が引数で渡されたitem_textと一致するか確認
        if self.scri_listbox.get(selected_indices[0]).strip() != item_text.strip():
            # これは右クリックメニューから呼ばれた場合のみ発生しうる
            # ボタンクリックの場合はcurselection()とitem_textが一致するはず
            messagebox.showwarning("警告", "選択された項目と削除対象の項目が一致しません。")
            return

        confirm = messagebox.askyesno(
            "削除の確認",
            f"'{item_text[:50]}...' を定型文から削除しますか?\nこの操作は元に戻せません。"
        )
        if not confirm:
            return

        self.scri_listbox.delete(selected_indices[0])
        messagebox.showinfo("成功", f"'{item_text[:30]}...' を定型文から削除しました。")
        self.save_scri_file_from_listbox() # 変更をファイルに保存

    def move_scri_item(self, direction):
        """
        定型文リストボックスで選択された項目を移動する。
        :param direction: -1 で上に移動、 1 で下に移動。
        """
        selected_indices = self.scri_listbox.curselection()
        if not selected_indices:
            return

        current_index = selected_indices[0]
        new_index = current_index + direction

        if 0 <= new_index < self.scri_listbox.size():
            item_to_move = self.scri_listbox.get(current_index)
            self.scri_listbox.delete(current_index)
            self.scri_listbox.insert(new_index, item_to_move)
            self.scri_listbox.selection_set(new_index)
            self.scri_listbox.activate(new_index)
            self.scri_listbox.see(new_index) # 移動後、アイテムが見えるようにスクロール

            self.save_scri_file_from_listbox() # 変更をファイルに保存
        else:
            messagebox.showinfo("情報", "これ以上移動できません。")

    def save_scri_file_from_listbox(self):
        """
        現在のリストボックスの内容を定型文ファイルに保存する。
        """
        if not self.current_scri_file:
            messagebox.showwarning("警告", "保存対象の定型文ファイルが特定できません。")
            return False

        scri_filepath = self.current_scri_file
        if not os.path.isabs(scri_filepath):
            scri_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_filepath)

        try:
            with open(scri_filepath, 'w', encoding='utf-8') as f:
                for i in range(self.scri_listbox.size()):
                    line = self.scri_listbox.get(i).strip()
                    if line: # 空行は保存しない
                        f.write(line + '\n')
            # messagebox.showinfo("保存完了", f"定型文の順序を '{os.path.basename(scri_filepath)}' に保存しました。")
            return True
        except Exception as e:
            messagebox.showerror("エラー", f"定型文ファイルの保存に失敗しました:\n{e}")
            return False

    def copy_editor_content_to_clipboard(self):
        """Copies all content from the editor to the clipboard."""
        editor_content = self.text_editor.get("1.0", tk.END).strip()

        if not editor_content:
            messagebox.showinfo("情報", "コピーする内容がありません。")
            return

        self.root.clipboard_clear()
        self.root.clipboard_append(editor_content)
        messagebox.showinfo("コピー完了", "エディタの内容がクリップボードにコピーされました。")

    def get_template_file_paths(self, template_name):
        """Returns the file paths for editor content and scri content for a given template."""
        editor_file = self.TEMPLATE_MAP.get(template_name)
        scri_file = self.SCRI_TEMPLATE_MAP.get(template_name)
        return editor_file, scri_file

    def load_file_content(self, filepath):
        """指定されたファイルパスのコンテンツをエディタにロードするヘルパーメソッド"""
        try:
            if not os.path.exists(filepath):
                messagebox.showerror("エラー", f"ファイルが見つかりません:\n{filepath}")
                return False

            with open(filepath, 'r', encoding='utf-8') as f:
                content = f.read()

            self.text_editor.delete("1.0", tk.END)
            self.text_editor.insert("1.0", content)
            self.current_file_path = filepath

            self.current_template = None
            self.current_scri_file = None # ファイルを開いた場合は定型文ファイルもリセット
            self.load_scri_file() # 定型文リストボックスをクリアまたは更新

            self.root.title(f"{os.path.basename(filepath)} - AI Script Helper Editor")
            self._update_line_numbers()
            self.text_editor.edit_modified(False)
            return True
        except Exception as e:
            messagebox.showerror("エラー", f"ファイルの読み込みに失敗しました:\n{e}")
            return False

    def set_template(self, template_name):
        """
        Loads the specified template, replaces editor content, and sets it as the current template.
        Also switches the fixed phrase file.
        """
        if self.text_editor.edit_modified():
            response = messagebox.askyesnocancel(
                "変更の保存",
                f"現在のファイル {os.path.basename(self.current_file_path or '(新規ファイル)')} が変更されています。保存しますか?"
            )
            if response is True:
                self.save_file()
            elif response is False:
                pass
            else:
                return

        editor_file, scri_file = self.get_template_file_paths(template_name)

        if not editor_file:
            messagebox.showerror("エラー", f"'{template_name}' に対応するエディタテンプレートファイルパスが見つかりません。settings.iniを確認してください。")
            self._reset_to_neutral_state()
            return
        if not scri_file:
            messagebox.showerror("エラー", f"'{template_name}' に対応する定型文ファイルパスが見つかりません。settings.iniを確認してください。")
            self._reset_to_neutral_state()
            return

        # Construct full paths for template files (assuming they are in the script's directory)
        editor_full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), editor_file)
        scri_full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_file)


        try:
            # Ensure template content file exists
            if not os.path.exists(editor_full_path):
                open(editor_full_path, 'w', encoding='utf-8').close()

            with open(editor_full_path, 'r', encoding='utf-8') as f:
                content = f.read()

            self.text_editor.delete("1.0", tk.END)
            self.text_editor.insert("1.0", content)
            self.root.title(f"{template_name} - AI Script Helper Editor")
            self._update_line_numbers()
            self.text_editor.edit_modified(False)

            self.current_template = template_name
            self.current_file_path = editor_full_path # テンプレートのファイルパスを設定
            self.current_scri_file = scri_full_path # テンプレートに対応する定型指示ファイルを設定
            self.load_scri_file()

            messagebox.showinfo("テンプレート変更", f"'{template_name}' テンプレートを読み込みました。")
            self.save_state_to_settings()
        except Exception as e:
            messagebox.showerror("エラー", f"テンプレート '{template_name}' の読み込みに失敗しました:\n{e}")
            self._reset_to_neutral_state()

    def _reset_to_neutral_state(self, skip_save_check=False):
        """エディタを中立的な状態(新規ファイル状態)にリセットするヘルパーメソッド"""
        self.text_editor.delete("1.0", tk.END)
        self.current_file_path = None
        self.current_template = None
        self.current_scri_file = None
        self.root.title("新規ファイル - AI Script Helper Editor")
        self._update_line_numbers()
        self.text_editor.edit_modified(False)
        self.load_scri_file() # 定型文リストボックスをクリア
        if not skip_save_check:
            self.save_state_to_settings()

    def new_file(self, skip_save_check=False):
        """Creates a new, blank file."""
        if not skip_save_check and self.text_editor.edit_modified():
            response = messagebox.askyesnocancel(
                "変更の保存",
                f"現在のファイル {os.path.basename(self.current_file_path or '(新規ファイル)')} が変更されています。保存しますか?"
            )
            if response is True:
                self.save_file()
            elif response is False:
                pass
            else:
                return
        self._reset_to_neutral_state(skip_save_check)

    def open_file(self):
        """Opens a file from disk."""
        if self.text_editor.edit_modified():
            response = messagebox.askyesnocancel(
                "変更の保存",
                f"現在のファイル {os.path.basename(self.current_file_path or '(新規ファイル)')} が変更されています。保存しますか?"
            )
            if response is True:
                self.save_file()
            elif response is False:
                pass
            else:
                return

        filepath = filedialog.askopenfilename(
            filetypes=[("Text Files", "*.txt"), ("Python Files", "*.py"), ("All Files", "*.*")]
        )
        if not filepath:
            return

        # ファイルを開く際はテンプレートとの紐付けを解除
        self.current_template = None
        if self.load_file_content(filepath):
            self.save_state_to_settings()

    def save_file(self):
        """Saves the current file, or prompts for save-as if it's a new file."""
        target_filepath = None
        if self.current_template:
            editor_file, _ = self.get_template_file_paths(self.current_template)
            if editor_file:
                target_filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), editor_file)
            else:
                messagebox.showerror("エラー", "テンプレートのファイルパスが不正です。名前を付けて保存します。")
                self.save_as_file()
                return

        elif self.current_file_path:
            target_filepath = self.current_file_path

        if target_filepath:
            try:
                with open(target_filepath, 'w', encoding='utf-8') as f:
                    f.write(self.text_editor.get("1.0", tk.END))
                messagebox.showinfo("成功", f"ファイルを保存しました:\n{target_filepath}")
                self.text_editor.edit_modified(False)
                self.save_state_to_settings()
            except Exception as e:
                messagebox.showerror("エラー", f"ファイルの保存に失敗しました:\n{e}")
        else:
            self.save_as_file()

    def save_as_file(self):
        """Saves the current file to a new location, prompting for filename."""
        initial_dir = self._get_my_documents_folder() # マイドキュメントのパスを取得
        current_datetime = datetime.datetime.now()

        # デフォルトファイル名を生成 (YYYYMMDD_HHMM_テンプレ名.txt)
        if self.current_template:
            # テンプレート名が日本語でもファイル名に使えるようにサニタイズ
            # Windowsでは一部記号がファイル名に使えないため、より厳密に
            sanitized_template_name = "".join(c for c in self.current_template if c.isalnum() or c in ('_', '-')).strip()
            if not sanitized_template_name:
                sanitized_template_name = "untitled" # テンプレート名が空の場合はデフォルト
            default_filename = current_datetime.strftime(f"%Y%m%d_%H%M_{sanitized_template_name}.txt")
        else:
            default_filename = current_datetime.strftime("%Y%m%d_%H%M_untitled.txt")

        filepath = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("Text Files", "*.txt"), ("All Files", "*.*")],
            initialdir=initial_dir,
            initialfile=default_filename # デフォルトファイル名を設定
        )
        if not filepath:
            return

        try:
            with open(filepath, 'w', encoding='utf-8') as f:
                f.write(self.text_editor.get("1.0", tk.END))
            messagebox.showinfo("成功", f"ファイルを保存しました:\n{filepath}")
            self.current_file_path = filepath
            self.current_template = None # 名前を付けて保存した場合はテンプレート紐付け解除
            self.text_editor.edit_modified(False)
            self.root.title(f"{os.path.basename(filepath)} - AI Script Helper Editor")
            self.save_state_to_settings()
        except Exception as e:
            messagebox.showerror("エラー", f"ファイルの保存に失敗しました:\n{e}")

    def _get_my_documents_folder(self):
        """OSに応じてマイドキュメントフォルダのパスを取得するヘルパー関数"""
        if platform.system() == "Windows":
            import winreg
            try:
                # Get the path to the My Documents folder on Windows
                # This works for "Documents" in modern Windows versions
                # For older systems, it might map to "My Documents"
                key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders")
                my_documents_path = winreg.QueryValueEx(key, "{FDD39AD0-238F-46AF-ADB4-6C85480369C7}")[0]
                winreg.CloseKey(key)
                return my_documents_path
            except Exception:
                # Fallback if registry access fails or key not found
                return os.path.expanduser("~/Documents")
        elif platform.system() == "Darwin": # macOS
            return os.path.expanduser("~/Documents")
        else: # Linux/Unix or other
            return os.path.expanduser("~/Documents")


    def save_file_with_date_and_template(self, include_time=False, to_my_documents=False):
        """
        現在のエディタ内容を、現在の日付と選択中のテンプレート名を含むファイル名で保存する。
        """
        if not self.current_template:
            messagebox.showwarning("警告", "日付ファイル保存にはテンプレートが選択されている必要があります。")
            return

        current_datetime = datetime.datetime.now()
        # テンプレート名をサニタイズ
        sanitized_template_name = "".join(c for c in self.current_template if c.isalnum() or c in ('_', '-')).strip()
        if not sanitized_template_name:
            sanitized_template_name = "untitled_template" # テンプレート名が空の場合の代替

        if include_time:
            filename = current_datetime.strftime(f"%Y%m%d_%H%M_{sanitized_template_name}.txt")
        else:
            filename = current_datetime.strftime(f"%Y%m%d_{sanitized_template_name}.txt")

        if to_my_documents:
            save_directory = self._get_my_documents_folder()
        else:
            # スクリプトがあるディレクトリ
            save_directory = os.path.dirname(os.path.abspath(__file__))

        save_path = os.path.join(save_directory, filename)

        try:
            with open(save_path, 'w', encoding='utf-8') as f:
                f.write(self.text_editor.get("1.0", tk.END))
            messagebox.showinfo("保存完了", f"ファイルを保存しました:\n{save_path}")
            self.text_editor.edit_modified(False)
            self.current_file_path = save_path # 新しい保存パスを現在のファイルとして設定
            self.root.title(f"{os.path.basename(save_path)} - AI Script Helper Editor")
            self.save_state_to_settings()
        except Exception as e:
            messagebox.showerror("エラー", f"ファイル '{filename}' の保存に失敗しました:\n{e}")

    # Search and Replace methods (Placeholders - Implement as needed)
    def find_text(self, event=None):
        """テキスト検索機能のプレースホルダー"""
        messagebox.showinfo("機能未実装", "検索機能はまだ実装されていません。")

    def replace_text(self, event=None):
        """テキスト置換機能のプレースホルダー"""
        messagebox.showinfo("機能未実装", "置換機能はまだ実装されていません。")

    def exit_editor(self):
        """アプリケーション終了時の処理"""
        if self.text_editor.edit_modified():
            response = messagebox.askyesnocancel(
                "変更の保存",
                f"現在のファイル {os.path.basename(self.current_file_path or '(新規ファイル)')} が変更されています。保存しますか?"
            )
            if response is True:
                self.save_file()
            elif response is False:
                pass
            else:
                return # Cancel exit

        self.save_state_to_settings() # 終了時に状態を保存
        self.root.destroy()

    def save_state_to_settings(self):
        """現在のエディタの状態をsettings.iniに保存する"""
        if not self.config.has_section('Settings'):
            self.config.add_section('Settings')

        if self.current_template:
            self.config.set('Settings', 'last_template', self.current_template)
        else:
            self.config.remove_option('Settings', 'last_template') # テンプレートが選択されていない場合は削除

        # ファイルパスは、テンプレートの場合はテンプレートファイル名を、それ以外はフルパスを保存
        if self.current_template and self.current_file_path:
            # テンプレートに紐づくファイルの場合、settings.iniにはテンプレートファイル名を保存
            editor_file_from_template, _ = self.get_template_file_paths(self.current_template)
            if editor_file_from_template:
                self.config.set('Settings', 'last_file_path', editor_file_from_template)
            else:
                self.config.remove_option('Settings', 'last_file_path')
        elif self.current_file_path:
            self.config.set('Settings', 'last_file_path', self.current_file_path)
        else:
            self.config.remove_option('Settings', 'last_file_path')

        # 定型文ファイルパスも同様に
        if self.current_template and self.current_scri_file:
            _, scri_file_from_template = self.get_template_file_paths(self.current_template)
            if scri_file_from_template:
                self.config.set('Settings', 'last_scri_file', scri_file_from_template)
            else:
                self.config.remove_option('Settings', 'last_scri_file')
        elif self.current_scri_file:
            self.config.set('Settings', 'last_scri_file', self.current_scri_file)
        else:
            self.config.remove_option('Settings', 'last_scri_file')

        try:
            with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
                self.config.write(f)
        except Exception as e:
            messagebox.showerror("エラー", f"設定の保存に失敗しました:\n{e}")

    def load_state_from_settings(self):
        """settings.iniから前回の状態を読み込む"""
        last_template = self.config.get('Settings', 'last_template', fallback=None)
        last_file_path = self.config.get('Settings', 'last_file_path', fallback=None)
        last_scri_file = self.config.get('Settings', 'last_scri_file', fallback=None)

        if last_template and last_template in self.TEMPLATE_MAP:
            # テンプレートが記憶されている場合
            editor_file_from_template, scri_file_from_template = self.get_template_file_paths(last_template)
            if editor_file_from_template:
                editor_full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), editor_file_from_template)
                scri_full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), scri_file_from_template)

                try:
                    with open(editor_full_path, 'r', encoding='utf-8') as f:
                        content = f.read()
                    self.text_editor.delete("1.0", tk.END)
                    self.text_editor.insert("1.0", content)
                    self.current_file_path = editor_full_path
                    self.current_template = last_template
                    self.current_scri_file = scri_full_path
                    self.load_scri_file()
                    self.root.title(f"{last_template} - AI Script Helper Editor")
                    messagebox.showinfo("起動", f"前回のテンプレート '{last_template}' を読み込みました。")
                except Exception as e:
                    messagebox.showwarning("起動エラー", f"前回のテンプレートファイル '{editor_file_from_template}' の読み込みに失敗しました:\n{e}\n\n新規ファイルとして開きます。")
                    self.new_file(skip_save_check=True) # エラーなので保存確認なしで新規ファイル
            else:
                self.new_file(skip_save_check=True) # テンプレートファイルパス不正
        elif last_file_path and os.path.exists(last_file_path):
            # テンプレートではなく、直接ファイルが記憶されている場合
            if self.load_file_content(last_file_path):
                messagebox.showinfo("起動", f"前回のファイル '{os.path.basename(last_file_path)}' を読み込みました。")
            else:
                self.new_file(skip_save_check=True) # 読み込み失敗
        else:
            # どちらも記憶されていない、またはファイルが見つからない場合は新規ファイル
            self.new_file(skip_save_check=True) # 起動時なので保存確認なし


def main():
    root = tk.Tk()
    app = ScriptHelperEditor(root)
    root.mainloop()

if __name__ == "__main__":
    main()

これで、AIプロンプト作成がよりスムーズになりますよ!

どんな風に使うの?

使い方はとってもシンプル!主に3つの機能が、あなたのAI活用をサポートしてくれます。

1. テンプレートでサッとスタート!

AIに何かお願いするときって、「文章作ってほしいな」「画像生成してほしいな」「この書類、校正してほしいな」みたいに、目的が決まっていることが多いですよね。このエディターには、そんな目的ごとにあらかじめ「型」が用意されています。

メニューから「コード作成」とか「文書校正」とか、やりたいことに合ったテンプレートを選ぶだけで、エディターに基本的な指示のひな形がパッと表示されます。これで「何から書こう?」って迷う時間がグッと減りますよ。

しかも、前回使ったテンプレートを覚えてくれるので、次にエディターを開いたときも、すぐに前回の続きから始められます!もちろん、あなただけのオリジナルテンプレートを作ることも可能です。

2. 定型文でポンと追加!

テンプレートを選んだら、次に活躍するのが定型文です。エディターの左側に、そのテンプレートに合わせた「よく使うフレーズ集」が表示されます。

例えば、コード作成のテンプレートなら、「Pythonで書いてね」「エラーハンドリングもお願い」といった、プログラミングでよく使う指示が並んでいるイメージです。使いたいフレーズをダブルクリックするだけで、エディターにサッと追加できます。キーボードを打つ手間が省けて、ストレスフリー!

「この表現、また使うかも!」と思ったら、エディターに書いた文章から、一行を選んで定型文に追加することもできます。どんどん自分だけの便利フレーズ集を育てていけますよ。

3. 日付ファイルで記録を残そう!

AIとのやり取りって、後から見返すと「あの時の指示、よかったな」とか「これは改善しよう」といった発見がありますよね。このエディターでは、あなたが作成した指示やAIからの応答を、分かりやすいファイル名で保存できます。

例えば、「20250704_文書校正.txt」のように、日付と使ったテンプレートの名前が自動でファイル名になるんです。時間を加えて「20250704_2315_文書校正.txt」と、より細かく記録することも可能!

保存先も、このエディターがあるフォルダか、あなたの「マイドキュメント」フォルダかを選べます。こうすることで、AIとどんなやり取りをしたのかを一目で把握できて、あなたのAI活用がどんどん賢くなっていきますよ。