streamlitでクリップボードにコピーボタンを実装

pythonでのコピーについても追記しました！(2024.08.04)

# やりたいこと
streamlit上で、AI生成したテキストをクリップボードにコピーできるボタンを設置したい！

クリップボードにコピーする処理だけ調べました。
Pythonを使う方法と、Javascrptを使う方法があります。

# Pythonを使ったシンプルなテキストコピー
`pyperclip`というモジュールを使うと良い。
```terminal
pip install pyperclip
```

```python
import pyperclip

text = "クリップボードにコピーしたいテキスト"
if st.button("コピー"):
    pyperclip.copy(text)
```
簡単:v:

しかし、問題が。
streamlitではボタンがクリックされるとアプリケーション全体を再実行されてしまうので、**画面が再読み込み**されてしまう。
「表示されている生成した文章をコピー」という要件的に、コピーボタンを押すと生成した文章が表示されなくなってしまうのは不便。

# Javascriptを使ったシンプルなテキストコピー
ということで、今回はJavascriptを使ったテキストコピーを採用します。
前回の記事でcomponents.htmlを使えばJavascriptを動かすことができることは学習済み！

https://qiita.com/aliceroot0678/items/6487eab357bef2c37eb7


```python:simple_copy.py
import streamlit as st
import streamlit.components.v1 as stc

# コピーするテキスト
copy_text = "これ、クリップボードにコピーされるよ！"

# HTMLテンプレート
html_content = f"""
<button onclick="navigator.clipboard.writeText('{copy_text}')">
    コピる！
</button>
"""

# 表示
stc.html(html_content, height=50)
st.write(copy_text)
```
録画ではtailwind効かせてるけど、画面はこんな感じ。やったー！:tada:

![画面収録 2024-08-02 23.51.24 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416489/dc658171-6601-f2cf-8357-0936fb91c4de.gif)


# Langchainで生成したテキストをコピー

次は、Langchainで生成したテキストをコピーできるように。
今回のコピーボタンはtailwind使ってちょっとリッチにしてみた
![スクリーンショット 2024-08-03 0.01.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416489/ec9908a1-08ea-11ab-99d2-ba87daf34aba.png)
`onclick`内の処理を実は少し変えてある。どこかな〜:eyes:

```python:langchain_copy.py
# 細かな初期化作業や生成部分は省略
generate_txt = chat([
        HumanMessage(content=prompt.format(article=generate_article.content))
])

# コピーするテキストを定義
copy_text = generate_txt.content

# HTMLテンプレートを作成
html_content = f"""
    <html>
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <script src="https://cdn.tailwindcss.com"></script>
        </head>
        <body>
            <div class="inline-flex border border-gray-200 rounded-full p-0.5 dark:border-neutral-700">
                <button onclick='navigator.clipboard.writeText({json.dumps(copy_text)})' type="button" class="inline-flex shrink-0 justify-center items-center size-8 rounded-full text-gray-500 hover:bg-blue-100 hover:text-blue-800 focus:outline-none focus:bg-blue-100 focus:text-blue-800 dark:text-neutral-500 dark:hover:bg-blue-900 dark:hover:text-blue-200 dark:focus:bg-blue-900 dark:focus:text-blue-200">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-copy"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7m0 2.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667z" /><path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" /></svg>
                </button>
            </div>
        </body>
    </html>
"""

# HTMLコンテンツを表示
stc.html(html_content,height=40)
st.write(generated_text.content)
```
**変えたところ**
`json.dumps()`の処理を追加しました。なぜこの変更が必要だったのか、説明していきます。

### つまづきポイント

Langchainで生成したテキストを単純にJavaScriptに渡そうとしたら、うまくいきませんでした。:sweat_smile:

エラーメッセージ：
```
SyntaxError: Invalid or unexpected token (at VM616 about:srcdoc:1:31)
```

注目すべきは、`pyperclip`を使用した場合は問題なくコピーできたという点です。これは、Pythonの世界内でのコピーだからです。

### 解決策：`json.dumps()`の使用

JavaScriptの`navigator.clipboard.writeText()`にPythonの変数を渡す際、`json.dumps()`を使用する必要があります。

```python
onclick='navigator.clipboard.writeText({json.dumps(copy_text)})'
```

#### なぜ`json.dumps()`が必要なのか？

1. **文字列のエスケープ**: Pythonの文字列には、JavaScriptが特別な意味を持つ文字（例：`'`, `"`、`\n`）が含まれている可能性があります。`json.dumps()`はこれらの文字を適切にエスケープします。

2. **Unicode処理**: `json.dumps()`は非ASCII文字を適切にエンコードします。

例えば：
```python
text = "Hello\nWorld"
print(json.dumps(text))  # 出力: "Hello\\nWorld"
```

この処理により、Pythonの文字列（改行や特殊文字を含む）がJavaScriptで安全に使用できる形に変換される！:relaxed:

この方法を使えば、Langchainで生成した複雑なテキストでも、問題なくクリップボードにコピーできるようになりました！:tada:

# まとめ

## 1. Pythonでコピーするなら
- `pyperclip`モジュール使えば簡単だよ！
- でも、Streamlitだとボタン押すたびにページ再読み込みしちゃう
- 動的に生成したテキストをコピーしたいときは使いにくいかも

## 2. JavaScriptでコピーするなら
- Streamlitの`components.html`使ってJavaScript埋め込める
- ページ再読み込みなしでコピーできるから便利！
- 動的なコンテンツのコピーによき:v:

### テキストの種類で違う
- 普通のテキスト: そのままJavaScriptに文字列渡せばOK
- Langchainとかで生成した複雑なテキスト: `json.dumps()`使おう

## 3. かっこよくしたいなら
- TailwindCSSあると、簡単にいい感じのボタン作れる！

## 4. どっち使う？
- 静的なテキストで再読み込みしても大丈夫なら: Python（`pyperclip`）
- 動的なテキストやUI反応良くしたいなら: JavaScript

これで、streamlit上にいい感じのコピーボタンが設置できたよ！AIが生成したテキストも簡単にコピーできるし結構使いやすくなったはず。